diff --git a/.aptly.conf b/.aptly.conf index cbea3aee1..deb53174e 100644 --- a/.aptly.conf +++ b/.aptly.conf @@ -25,7 +25,7 @@ "region": "eu01", "bucket": "distribution", "acl":"public-read", - "endpoint": "object.storage.eu01.onstackit.cloud" + "endpoint": "https://object.storage.eu01.onstackit.cloud" } }, "SwiftPublishEndpoints": {}, diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5400a08cb..3a4e09fe6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,12 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 + exclude: ["github.com/stackitcloud*"] - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 diff --git a/.github/docs/contribution-guide/client.go b/.github/docs/contribution-guide/client.go index 3fdda371b..df0f74442 100644 --- a/.github/docs/contribution-guide/client.go +++ b/.github/docs/contribution-guide/client.go @@ -2,46 +2,12 @@ package client import ( "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/foo" // (...) ) func ConfigureClient(p *print.Printer, cliVersion string) (*foo.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - return nil, &errors.AuthError{} - } - - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), // Configuring region is needed if "foo" is a regional API - authCfgOption, - } - - customEndpoint := viper.GetString(config.fooCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := foo.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.fooCustomEndpointKey), false, genericclient.CreateApiClient[*foo.APIClient](foo.NewAPIClient)) } diff --git a/.github/docs/contribution-guide/cmd.go b/.github/docs/contribution-guide/cmd.go index 175a9a6d3..1cb1109b9 100644 --- a/.github/docs/contribution-guide/cmd.go +++ b/.github/docs/contribution-guide/cmd.go @@ -2,11 +2,11 @@ package bar import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,14 +17,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "gopkg.in/yaml.v2" // (...) ) // Define consts for command flags const ( - someArg = "MY_ARG" - someFlag = "my-flag" + someArg = "MY_ARG" + someFlag = "my-flag" + secretFlag = "secret" ) // Struct to model user input (arguments and/or flags) @@ -32,10 +32,11 @@ type inputModel struct { *globalflags.GlobalFlagModel MyArg string MyFlag *string + Secret *string } // "bar" command constructor -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "bar", Short: "Short description of the command (is shown in the help of parent command)", @@ -86,8 +87,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure command flags (type, default value, and description) -func configureFlags(cmd *cobra.Command) { +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { cmd.Flags().StringP(someFlag, "shorthand", "defaultValue", "My flag description") + secret := flags.SecretFlag(secretFlag, params) + cmd.Flags().Var(secret, secretFlag, secret.Usage()) } // Parse user input (arguments and/or flags) @@ -103,18 +106,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu GlobalFlagModel: globalFlags, MyArg: myArg, MyFlag: flags.FlagToStringPointer(p, cmd, someFlag), + Secret: flags.SecretFlagToStringPointer(p, cmd, secretFlag), } // Write the input model to the debug logs - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -126,22 +122,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *foo.APIClie // Output result based on the configured output format func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, resources []foo.Resource) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resources, "", " ") - if err != nil { - return fmt.Errorf("marshal resource list: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.Marshal(resources) - if err != nil { - return fmt.Errorf("marshal resource list: %w", err) - } - p.Outputln(string(details)) - return nil - default: + // the output result handles JSON/YAML output, you can pass your own callback func for pretty (default) output format + return p.OutputResult(outputFormat, resources, func() error { table := tables.NewTable() table.SetHeader("ID", "NAME", "STATE") for i := range resources { @@ -153,5 +135,5 @@ func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, res return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/.github/renovate.json b/.github/renovate.json deleted file mode 100644 index 31621a1ba..000000000 --- a/.github/renovate.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"], - "prHourlyLimit": 10, - "labels": ["renovate"], - "repositories": ["stackitcloud/stackit-cli"], - "enabledManagers": ["gomod", "github-actions"], - "packageRules": [ - { - "matchSourceUrls": ["https://github.com/stackitcloud/stackit-sdk-go"], - "groupName": "STACKIT SDK modules" - } - ], - "postUpdateOptions": ["gomodTidy", "gomodUpdateImportPaths"] -} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a9a3dd3fa..a944446b0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,10 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7.0.0 - name: Install go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7.0.0 with: go-version-file: "go.mod" cache: true @@ -36,7 +36,7 @@ jobs: run: make test - name: Archive code coverage results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: ${{ env.CODE_COVERAGE_ARTIFACT_NAME }} path: ${{ env.CODE_COVERAGE_FILE_NAME }} @@ -47,10 +47,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7.0.0 - name: Check GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7.2.3 with: args: check @@ -65,7 +65,7 @@ jobs: pull-requests: write # write permission needed to comment on PR steps: - name: Check new code coverage - uses: fgrosse/go-coverage-report@v1.2.0 + uses: fgrosse/go-coverage-report@v1.3.0 continue-on-error: true # Add this line to prevent pipeline failures in forks with: coverage-artifact-name: ${{ env.CODE_COVERAGE_ARTIFACT_NAME }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 1954acadd..5994f9280 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -21,26 +21,35 @@ jobs: runs-on: macOS-latest env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }} - # Needed to publish new packages to our S3-hosted APT repo - AWS_ACCESS_KEY_ID: ${{ secrets.OBJECT_STORAGE_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SECRET_ACCESS_KEY }} steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v7.0.0 with: # Allow goreleaser to access older tag information. fetch-depth: 0 + - name: Install go - uses: actions/setup-go@v5 + uses: actions/setup-go@v7.0.0 with: go-version-file: "go.mod" cache: true + - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 id: import_gpg with: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} + + # nfpm-rpm signing needs gpg provided as filepath + # https://goreleaser.com/customization/nfpm/ + - name: Create GPG key file + run: | + KEY_PATH="$RUNNER_TEMP/gpg-private-key.asc" + printf '%s' "${{ secrets.GPG_PRIVATE_KEY }}" > "$KEY_PATH" + chmod 600 "$KEY_PATH" + echo "GPG_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV" + - name: Set up keychain run: | echo -n $SIGNING_CERTIFICATE_BASE64 | base64 -d -o ./ApplicationID.p12 @@ -61,23 +70,103 @@ jobs: APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} SIGNING_CERTIFICATE_BASE64: ${{ secrets.APPLICATION_ID_CERT }} AUTHKEY_BASE64: ${{ secrets.APPLE_API_KEY }} - # aptly version 1.6.0 results in an segmentation fault. Therefore we fall back to version 1.5.0. - # Since it is not possible to specify a version via brew command a formula was added for aptly 1.5.0 - # (source: https://github.com/Homebrew/homebrew-core/pull/202415/files) - - name: Install Aptly version 1.5.0 - run: brew install aptly.rb - name: Install Snapcraft uses: samuelmeuli/action-snapcraft@v3 + - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7.2.3 with: args: release --clean env: GITHUB_TOKEN: ${{ secrets.CLI_RELEASE }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GPG_KEY_PATH: ${{ env.GPG_KEY_PATH }} + # nfpm-rpm signing needs this env to be set. + NFPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + + - name: Clean up GPG key file + if: always() + run: | + rm -f "$GPG_KEY_PATH" + + - name: Upload artifacts to workflow + uses: actions/upload-artifact@v7 + with: + name: goreleaser-dist-temp + path: dist + retention-days: 1 + + publish-apt: + name: Publish APT + runs-on: macOS-latest + needs: [goreleaser] + env: + # Needed to publish new packages to our S3-hosted APT repo + AWS_ACCESS_KEY_ID: ${{ secrets.OBJECT_STORAGE_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SECRET_ACCESS_KEY }} + steps: + - name: Checkout + uses: actions/checkout@v7.0.0 + + # use the artifacts from the "goreleaser" job + - name: Download artifacts from workflow + uses: actions/download-artifact@v8 + with: + name: goreleaser-dist-temp + path: dist + + - name: Install Aptly + run: brew install aptly + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v7 + id: import_gpg + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + - name: Publish packages to APT repo if: contains(github.ref_name, '-') == false env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} GPG_PRIVATE_KEY_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} run: ./scripts/publish-apt-packages.sh + + publish-rpm: + name: Publish RPM + runs-on: ubuntu-latest + needs: [goreleaser] + env: + # Needed to publish new packages to our S3-hosted RPM repo + AWS_ACCESS_KEY_ID: ${{ secrets.OBJECT_STORAGE_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: eu01 + AWS_ENDPOINT_URL: https://object.storage.eu01.onstackit.cloud + steps: + - name: Checkout + uses: actions/checkout@v7.0.0 + + - name: Download artifacts from workflow + uses: actions/download-artifact@v8 + with: + name: goreleaser-dist-temp + path: dist + + - name: Install RPM tools + run: | + sudo apt-get update + sudo apt-get install -y createrepo-c + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v7 + id: import_gpg + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Publish RPM packages + if: contains(github.ref_name, '-') == false + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + run: ./scripts/publish-rpm-packages.sh \ No newline at end of file diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml deleted file mode 100644 index f2578c438..000000000 --- a/.github/workflows/renovate.yaml +++ /dev/null @@ -1,19 +0,0 @@ -name: Renovate - -on: - schedule: - - cron: "0 0 * * *" - workflow_dispatch: - -jobs: - renovate: - name: Renovate - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v5 - - name: Self-hosted Renovate - uses: renovatebot/github-action@v43.0.8 - with: - configurationFile: .github/renovate.json - token: ${{ secrets.RENOVATE_TOKEN }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 3ee54f0da..814490cfb 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -11,6 +11,7 @@ on: env: DAYS_BEFORE_PR_STALE: 7 DAYS_BEFORE_PR_CLOSE: 7 + EXEMPT_PR_LABELS: "ignore-stale" permissions: issues: write @@ -23,13 +24,14 @@ jobs: timeout-minutes: 10 steps: - name: "Mark old PRs as stale" - uses: actions/stale@v9 + uses: actions/stale@v10.4.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-pr-message: "This PR was marked as stale after ${{ env.DAYS_BEFORE_PR_STALE }} days of inactivity and will be closed after another ${{ env.DAYS_BEFORE_PR_CLOSE }} days of further inactivity. If this PR should be kept open, just add a comment, remove the stale label or push new commits to it." close-pr-message: "This PR was closed automatically because it has been stalled for ${{ env.DAYS_BEFORE_PR_CLOSE }} days with no activity. Feel free to re-open it at any time." days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-pr-labels: ${{ env.EXEMPT_PR_LABELS }} # never mark issues as stale or close them days-before-issue-stale: -1 days-before-issue-close: -1 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index f8c772377..7fb43c81d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -48,25 +48,33 @@ builds: - cmd: spctl -a -t open --context context:primary-signature -v dist/{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.dmg output: true # Completion files - - cmd: mkdir -p dist/macos-builds_{{.Target}}/completions - - cmd: sh -c './dist/macos-builds_{{.Target}}/{{.Name}} completion zsh > ./dist/macos-builds_{{.Target}}/completions/stackit.zsh' - - cmd: sh -c './dist/macos-builds_{{.Target}}/{{.Name}} completion bash > ./dist/macos-builds_{{.Target}}/completions/stackit.bash' - - cmd: sh -c './dist/macos-builds_{{.Target}}/{{.Name}} completion fish > ./dist/macos-builds_{{.Target}}/completions/stackit.fish' + - cmd: mkdir -p dist/completions + - cmd: sh -c 'go run main.go completion zsh > ./dist/completions/stackit.zsh' + - cmd: sh -c 'go run main.go completion bash > ./dist/completions/stackit.bash' + - cmd: sh -c 'go run main.go completion fish > ./dist/completions/stackit.fish' + + - id: freebsd-builds + env: + - CGO_ENABLED=0 + goos: + - freebsd + goarch: + - arm64 + - amd64 + binary: "stackit" archives: + - id: windows-archives + ids: + - windows-builds + formats: [ 'zip' ] - ids: - linux-builds - - windows-builds - formats: [ 'tar.gz' ] - format_overrides: - - goos: windows - formats: [ 'zip' ] - - id: macos-archives - ids: - macos-builds + - freebsd-builds formats: [ 'tar.gz' ] files: - - src: ./dist/macos-builds_{{.Target}}/completions/* + - src: ./dist/completions/* dst: completions - LICENSE.md - README.md @@ -97,28 +105,21 @@ nfpms: license: Apache 2.0 contents: - src: LICENSE.md - dst: LICENSE.md + dst: /usr/share/doc/stackit/copyright + file_info: + mode: 0644 formats: - deb - rpm -signs: - - artifacts: package - args: - [ - "-u", - "{{ .Env.GPG_FINGERPRINT }}", - "--output", - "${signature}", - "--detach-sign", - "${artifact}", - ] + rpm: + # The package is signed if a key_file is set + signature: + key_file: "{{ .Env.GPG_KEY_PATH }}" homebrew_casks: - name: stackit directory: Casks - conflicts: - - formula: stackit repository: owner: stackitcloud name: homebrew-tap @@ -126,7 +127,7 @@ homebrew_casks: name: CLI Release Bot email: noreply@stackit.de homepage: "https://github.com/stackitcloud/stackit-cli" - description: "A command-line interface to manage STACKIT resources.\nThis CLI is in a beta state. More services and functionality will be supported soon." + description: "A command-line interface to manage STACKIT resources." license: "Apache-2.0" # If set to auto, the release will not be uploaded to the homebrew tap repo # if the tag has a prerelease indicator (e.g. v0.0.1-alpha1) @@ -146,12 +147,12 @@ snapcrafts: # centre graphical frontends title: STACKIT CLI summary: A command-line interface to manage STACKIT resources. - description: "A command-line interface to manage STACKIT resources.\nThis CLI is in a beta state. More services and functionality will be supported soon." + description: "A command-line interface to manage STACKIT resources." license: Apache-2.0 confinement: classic # Grade "devel" will only release to `edge` and `beta` channels # Grade "stable" will also release to the `candidate` and `stable` channels - grade: devel + grade: stable # Whether to publish the Snap to the store publish: true diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md index 1ec7ea3ba..21cfd2833 100644 --- a/AUTHENTICATION.md +++ b/AUTHENTICATION.md @@ -4,7 +4,7 @@ This document describes how you can configure authentication for the STACKIT CLI ## Service account -You can use a [service account](https://docs.stackit.cloud/stackit/en/service-accounts-134415819.html) to authenticate to the STACKIT CLI. +You can use a [service account](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/) to authenticate to the STACKIT CLI. The CLI will search for service account credentials similarly to the [STACKIT SDK](https://github.com/stackitcloud/stackit-sdk-go) and [STACKIT Terraform Provider](https://github.com/stackitcloud/terraform-provider-stackit), so if you have already set up your environment for those tools, you can just run: ```bash @@ -47,14 +47,14 @@ To use the key flow, you need to have a service account key, which must have an When creating the service account key, a new RSA key-pair can be created automatically, which will be included in the service account key. This will make it much easier to configure the key flow authentication in the CLI, by just providing the service account key. -**Optionally**, you can provide your own private key when creating the service account key, which will then require you to also provide it explicitly to the CLI, additionally to the service account key. Check the STACKIT Knowledge Base for an [example of how to create your own key-pair](https://docs.stackit.cloud/stackit/en/usage-of-the-service-account-keys-in-stackit-175112464.html#UsageoftheserviceaccountkeysinSTACKIT-CreatinganRSAkey-pair). +**Optionally**, you can provide your own private key when creating the service account key, which will then require you to also provide it explicitly to the CLI, additionally to the service account key. Check the STACKIT Docs for an [example of how to create your own key-pair](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/how-tos/manage-service-account-keys/). To configure the key flow, follow this steps: 1. Create a service account key: - In the CLI, run `stackit service-account key create --email ` -- As an alternative, use the [STACKIT Portal](https://portal.stackit.cloud/): go to the `Service Accounts` tab, choose a `Service Account` and go to `Service Account Keys` to create a key. For more details, see [Create a service account key](https://docs.stackit.cloud/stackit/en/create-a-service-account-key-175112456.html) +- As an alternative, use the [STACKIT Portal](https://portal.stackit.cloud/): go to the `Service Accounts` tab, choose a `Service Account` and go to `Service Account Keys` to create a key. For more details, see [Create a service account key](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/how-tos/manage-service-account-keys/) 2. Save the content of the service account key by copying it and saving it in a JSON file. @@ -92,7 +92,12 @@ The expected format of the service account key is a **json** with the following > - setting the environment variable `STACKIT_PRIVATE_KEY_PATH` > - setting `STACKIT_PRIVATE_KEY_PATH` in the credentials file (see above) -4. The CLI will search for the keys and, if valid, will use them to get access and refresh tokens which will be used to authenticate all the requests. +4. Alternative, if you want to pass the keys directly without storing a file on disk: + + - setting the environment variable `STACKIT_SERVICE_ACCOUNT_KEY` with the content of the service account key + - optional: setting the environment variable `STACKIT_PRIVATE_KEY` with the content of the private key + +5. The CLI will search for the keys and, if valid, will use them to get access and refresh tokens which will be used to authenticate all the requests. ### Token flow @@ -101,3 +106,28 @@ Using this flow is less secure since the token is long-lived. You can provide th 1. Providing the flag `--service-account-token` 2. Setting the environment variable `STACKIT_SERVICE_ACCOUNT_TOKEN` 3. Setting `STACKIT_SERVICE_ACCOUNT_TOKEN` in the credentials file (see above) + +### Workload Identity Federation (OIDC) + +1. Create a service account trusted relation in the STACKIT Portal: + + - Navigate to `Service Accounts` → Select account → `Federated Identity Providers` + - [Configure a Federated Identity Provider](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/how-tos/manage-service-account-federations/#create-a-federated-identity-provider) and the required assertions. For detailed assertion configuration per platform, see the [Terraform provider WIF guide](https://github.com/stackitcloud/terraform-provider-stackit/blob/main/docs/guides/workload_identity_federation.md). + +2. Configure authentication for `stackit auth activate-service-account` using one of the options below: + + - Explicit flag: `--use-oidc` (takes precedence) + - Environment variable: `STACKIT_USE_OIDC=1` + + If both are provided, the explicit flag value is used. + + Example using environment variables: + + ```bash + STACKIT_USE_OIDC=1 + STACKIT_SERVICE_ACCOUNT_EMAIL=my-sa@sa.stackit.cloud + # Optional: provide the OIDC token directly instead of auto-detecting it from the CI environment + STACKIT_SERVICE_ACCOUNT_FEDERATED_TOKEN= + # Optional: provide a file path containing the OIDC token + STACKIT_FEDERATED_TOKEN_FILE=/path/to/oidc-token.jwt + ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..8e987e78a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,141 @@ +# Contribute to the STACKIT CLI + +Your contribution is welcome! Thank you for your interest in contributing to the STACKIT CLI. We greatly value your feedback, feature requests, additions to the code, bug reports or documentation extensions. + +## Table of contents + +- [Developer Guide](#developer-guide) +- [Useful Make commands](#useful-make-commands) +- [Repository structure](#repository-structure) +- [Implementing a new command](#implementing-a-new-command) + - [Command file structure](#command-file-structure) + - [Outputs, prints and debug logs](#outputs-prints-and-debug-logs) +- [Onboarding a new STACKIT service](#onboarding-a-new-stackit-service) +- [Local development](#local-development) +- [Code Contributions](#code-contributions) +- [Bug Reports](#bug-reports) + +## Developer Guide + +Prerequisites: + +- [`Go`](https://go.dev/doc/install) 1.24+ +- [`yamllint`](https://yamllint.readthedocs.io/en/stable/quickstart.html) + +### Useful Make commands + +These commands can be executed from the project root: + +- `make build`: compile the CLI and save the binary under _./bin/stackit_ +- `make lint`: lint the code +- `make generate-docs`: generate Markdown documentation for every command +- `make test`: run unit tests +- `make coverage`: create unit test coverage report (output file: `coverage.html`) + +### Repository structure + +The CLI commands are located under `internal/cmd`, where each folder includes the source code for each subcommand (including their own subcommands). Inside `pkg` you can find several useful packages that are shared by the commands and provide additional functionality such as `flags`, `globalflags`, `tables`, etc. + +### Implementing a new command + +Let's suppose you want to implement a new command `bar`, that would be the direct child of an existing command `stackit foo` (meaning it would be invoked as `stackit foo bar`): + +1. You would start by creating a new folder `bar/` inside `internal/cmd/foo/` +2. Following with the creation of a file `bar.go` inside your new folder `internal/cmd/foo/bar/` + 1. The Go package should be similar to the command usage, in this case `package bar` would be an adequate name + 2. Please refer to the [Command file structure](./CONTRIBUTING.md/#command-file-structure) section for details on the structure of the file itself +3. To register the command `bar` as a child of the existing command `foo`, add `cmd.AddCommand(bar.NewCmd(p))` to the `addSubcommands` method of the constructor of the `foo` command + 1. In this case, `p` is the `printer` that is passed from the root command to all subcommands of the tree (refer to the [Outputs, prints and debug logs](./CONTRIBUTING.md/#outputs-prints-and-debug-logs) section for more details regarding the `printer`) + +Please remember to run `make generate-docs` after your changes to keep the commands' documentation updated. + +#### Command file structure + +Below is a typical structure of a CLI command: + +https://github.com/stackitcloud/stackit-cli/blob/main/.github/docs/contribution-guide/cmd.go + +Please remember to always add unit tests for `parseInput`, `buildRequest` (in `bar_test.go`), and any other util functions used. + +If the new command `bar` is the first command in the CLI using a STACKIT service `foo`, please refer to [Onboarding a new STACKIT service](./CONTRIBUTING.md/#onboarding-a-new-stackit-service). + +You may also have to register the `bar` command as a new sub-command: + +https://github.com/stackitcloud/stackit-cli/blob/a5438f4cac3a794cb95d04891a83252aa9ae1f1e/internal/cmd/root.go#L162-L195 + +#### Outputs, prints and debug logs + +The CLI has 4 different verbosity levels: + +- `error`: For only displaying errors +- `warning`: For displaying user facing warnings _(and all of the above)_ +- `info` (default): For displaying user facing info, such as operation success messages and spinners _(and all of the above)_ +- `debug`: For displaying structured logs with different levels, including errors _(and all of the above)_ + +For prints that are specific to a certain log level, you can use the methods defined in the `print` package: `Error`, `Warn`, `Info`, and `Debug`. + +For command outputs that should always be displayed, no matter the defined verbosity, you should use the `print` methods `Outputf` and `Outputln`. These should only be used for the actual output of the commands, which can usually be described by "I ran the command to see _this_". + +#### Handling secrets + +If your command needs secrets as input, please make sure to use `flags.SecretFlag()` and `flags.SecretFlagToStringPointer()`. +These functions implement reading from stdin or a file. + +They also support reading the secret value as a command line argument (deprecated, marked for removal in Oct 2026). + +### Onboarding a new STACKIT service + +If you want to add a command that uses a STACKIT service `foo` that was not yet used by the CLI, you will first need to implement a few extra steps to configure the new service: + +1. Add a `FooCustomEndpointKey` key in `internal/pkg/config/config.go` (and add it to `ConfigKeys` and set the to default to `""` using `viper.SetDefault`) +2. Update the `stackit config unset` and `stackit config unset` commands by adding flags to set and unset a custom endpoint for the `foo` service API, respectively, and update their unit tests +3. Set up the SDK client configuration, using the authentication method configured in the CLI + + 1. This is done in `internal/pkg/services/foo/client/client.go` + 2. Below is an example of a typical `client.go` file structure: + +https://github.com/stackitcloud/stackit-cli/blob/main/.github/docs/contribution-guide/client.go + +### Local development + +To test your changes, you can either: + +1. Build the application locally by running: + + ```bash + $ go build -o ./bin/stackit + ``` + + To use the application from the root of the repository, you can run: + + ```bash + $ ./bin/stackit [group] [subgroup] [command] [flags] + ``` + +2. Skip building and run the Go application directly using: + + ```bash + $ go run . [group] [subgroup] [command] [flags] + ``` + +## Code Contributions + +To make your contribution, follow these steps: + +1. Check open or recently closed [Pull Requests](https://github.com/stackitcloud/stackit-cli/pulls) and [Issues](https://github.com/stackitcloud/stackit-cli/issues) to make sure the contribution you are making has not been already tackled by someone else. +2. Fork the repo. +3. Make your changes in a branch that is up-to-date with the original repo's `main` branch. +4. Commit your changes including a descriptive message (see our [commit message guide](https://github.com/stackitcloud/terraform-provider-stackit/blob/main/CONTRIBUTING.md#commit-messages) in the STACKIT Terraform provider repository). +5. Create a pull request with your changes. +6. The pull request will be reviewed by the repo maintainers. If you need to make further changes, make additional commits to keep commit history. When the PR is merged, commits will be squashed. + +## Bug Reports + +If you would like to report a bug, please open a [GitHub issue](https://github.com/stackitcloud/stackit-cli/issues/new). + +To ensure we can provide the best support to your issue, follow these guidelines: + +1. Go through the existing issues to check if your issue has already been reported. +2. Make sure you are using the latest version of the STACKIT CLI, we will not provide bug fixes for older versions. Also, latest versions may have the fix for your bug. +3. Please provide as much information as you can about your environment, e.g. your version of Go, your version of the CLI, which operating system you are using and the corresponding version. +4. Include in your issue the steps to reproduce it, along with code snippets and/or information about your specific use case. This will make the support process much easier and efficient. diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index e380d0458..409323c79 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -1,130 +1,6 @@ -# Contribute to the STACKIT CLI +# Moved -Your contribution is welcome! Thank you for your interest in contributing to the STACKIT CLI. We greatly value your feedback, feature requests, additions to the code, bug reports or documentation extensions. +Our contribution guide has moved to [CONTRIBUTING.md](./CONTRIBUTING.md). -## Table of contents +This way we stick to GitHub's standards: [Setting guidelines for repository contributors](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors). -- [Developer Guide](#developer-guide) -- [Useful Make commands](#useful-make-commands) -- [Repository structure](#repository-structure) -- [Implementing a new command](#implementing-a-new-command) - - [Command file structure](#command-file-structure) - - [Outputs, prints and debug logs](#outputs-prints-and-debug-logs) -- [Onboarding a new STACKIT service](#onboarding-a-new-stackit-service) -- [Local development](#local-development) -- [Code Contributions](#code-contributions) -- [Bug Reports](#bug-reports) - -## Developer Guide - -Prerequisites: - -- [`Go`](https://go.dev/doc/install) 1.24+ -- [`yamllint`](https://yamllint.readthedocs.io/en/stable/quickstart.html) - -### Useful Make commands - -These commands can be executed from the project root: - -- `make build`: compile the CLI and save the binary under _./bin/stackit_ -- `make lint`: lint the code -- `make generate-docs`: generate Markdown documentation for every command -- `make test`: run unit tests -- `make coverage`: create unit test coverage report (output file: `coverage.html`) - -### Repository structure - -The CLI commands are located under `internal/cmd`, where each folder includes the source code for each subcommand (including their own subcommands). Inside `pkg` you can find several useful packages that are shared by the commands and provide additional functionality such as `flags`, `globalflags`, `tables`, etc. - -### Implementing a new command - -Let's suppose you want to implement a new command `bar`, that would be the direct child of an existing command `stackit foo` (meaning it would be invoked as `stackit foo bar`): - -1. You would start by creating a new folder `bar/` inside `internal/cmd/foo/` -2. Following with the creation of a file `bar.go` inside your new folder `internal/cmd/foo/bar/` - 1. The Go package should be similar to the command usage, in this case `package bar` would be an adequate name - 2. Please refer to the [Command file structure](./CONTRIBUTION.md/#command-file-structure) section for details on the structure of the file itself -3. To register the command `bar` as a child of the existing command `foo`, add `cmd.AddCommand(bar.NewCmd(p))` to the `addSubcommands` method of the constructor of the `foo` command - 1. In this case, `p` is the `printer` that is passed from the root command to all subcommands of the tree (refer to the [Outputs, prints and debug logs](./CONTRIBUTION.md/#outputs-prints-and-debug-logs) section for more details regarding the `printer`) - -Please remember to run `make generate-docs` after your changes to keep the commands' documentation updated. - -#### Command file structure - -Below is a typical structure of a CLI command: - -https://github.com/stackitcloud/stackit-cli/blob/85ce44cd18d11169f2548d8657031b5fc6f94740/.github/docs/contribution-guide/cmd.go#L23-L156 - -Please remember to always add unit tests for `parseInput`, `buildRequest` (in `bar_test.go`), and any other util functions used. - -If the new command `bar` is the first command in the CLI using a STACKIT service `foo`, please refer to [Onboarding a new STACKIT service](./CONTRIBUTION.md/#onboarding-a-new-stackit-service). - -#### Outputs, prints and debug logs - -The CLI has 4 different verbosity levels: - -- `error`: For only displaying errors -- `warning`: For displaying user facing warnings _(and all of the above)_ -- `info` (default): For displaying user facing info, such as operation success messages and spinners _(and all of the above)_ -- `debug`: For displaying structured logs with different levels, including errors _(and all of the above)_ - -For prints that are specific to a certain log level, you can use the methods defined in the `print` package: `Error`, `Warn`, `Info`, and `Debug`. - -For command outputs that should always be displayed, no matter the defined verbosity, you should use the `print` methods `Outputf` and `Outputln`. These should only be used for the actual output of the commands, which can usually be described by "I ran the command to see _this_". - -### Onboarding a new STACKIT service - -If you want to add a command that uses a STACKIT service `foo` that was not yet used by the CLI, you will first need to implement a few extra steps to configure the new service: - -1. Add a `FooCustomEndpointKey` key in `internal/pkg/config/config.go` (and add it to `ConfigKeys` and set the to default to `""` using `viper.SetDefault`) -2. Update the `stackit config unset` and `stackit config unset` commands by adding flags to set and unset a custom endpoint for the `foo` service API, respectively, and update their unit tests -3. Set up the SDK client configuration, using the authentication method configured in the CLI - - 1. This is done in `internal/pkg/services/foo/client/client.go` - 2. Below is an example of a typical `client.go` file structure: - -https://github.com/stackitcloud/stackit-cli/blob/85ce44cd18d11169f2548d8657031b5fc6f94740/.github/docs/contribution-guide/client.go#L12-L35 - -### Local development - -To test your changes, you can either: - -1. Build the application locally by running: - - ```bash - $ go build -o ./bin/stackit - ``` - - To use the application from the root of the repository, you can run: - - ```bash - $ ./bin/stackit [group] [subgroup] [command] [flags] - ``` - -2. Skip building and run the Go application directly using: - - ```bash - $ go run . [group] [subgroup] [command] [flags] - ``` - -## Code Contributions - -To make your contribution, follow these steps: - -1. Check open or recently closed [Pull Requests](https://github.com/stackitcloud/stackit-cli/pulls) and [Issues](https://github.com/stackitcloud/stackit-cli/issues) to make sure the contribution you are making has not been already tackled by someone else. -2. Fork the repo. -3. Make your changes in a branch that is up-to-date with the original repo's `main` branch. -4. Commit your changes including a descriptive message -5. Create a pull request with your changes. -6. The pull request will be reviewed by the repo maintainers. If you need to make further changes, make additional commits to keep commit history. When the PR is merged, commits will be squashed. - -## Bug Reports - -If you would like to report a bug, please open a [GitHub issue](https://github.com/stackitcloud/stackit-cli/issues/new). - -To ensure we can provide the best support to your issue, follow these guidelines: - -1. Go through the existing issues to check if your issue has already been reported. -2. Make sure you are using the latest version of the STACKIT CLI, we will not provide bug fixes for older versions. Also, latest versions may have the fix for your bug. -3. Please provide as much information as you can about your environment, e.g. your version of Go, your version of the CLI, which operating system you are using and the corresponding version. -4. Include in your issue the steps to reproduce it, along with code snippets and/or information about your specific use case. This will make the support process much easier and efficient. diff --git a/INSTALLATION.md b/INSTALLATION.md index 965ceddf9..a20cf097e 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,60 +4,30 @@ [![Packaging status](https://repology.org/badge/vertical-allrepos/stackit-cli.svg?columns=1)](https://repology.org/project/stackit-cli/versions) -### macOS - -The STACKIT CLI can be installed through the [Homebrew](https://brew.sh/) package manager. - -1. First, you need to register the [STACKIT tap](https://github.com/stackitcloud/homebrew-tap) via: +### Platform independent -```shell -brew tap stackitcloud/tap -``` +#### mise -2. You can then install the CLI via: +The STACKIT CLI is available on all major platforms via [mise](https://mise.jdx.dev/): ```shell -brew install --cask stackit +mise u -g github:stackitcloud/stackit-cli ``` -#### Formula deprecated - -The homebrew formula is deprecated, will no longer be updated and will be removed after 2026-01-22. -You need to install the STACKIT CLI as cask. -Therefor you need to uninstall the formula and reinstall it as cask. - -Your profiles should normally remain. To ensure that nothing will be gone, you should backup them. - -1. Export your existing profiles. This will create a json file in your current directory. -```shell -stackit config profile export default -``` - -2. If you have multiple profiles, then execute the export command for each of them. You can find your profiles via: +### macOS -```shell -stackit config profile list -stackit config profile export -``` +The STACKIT CLI can be installed through the [Homebrew](https://brew.sh/) package manager. -3. Uninstall the formula. -```shell -brew uninstall stackit -``` +1. First, you need to register the [STACKIT tap](https://github.com/stackitcloud/homebrew-tap) via: -4. Install the STACKIT CLI as cask. ```shell -brew install --cask stackit +brew tap stackitcloud/tap ``` -5. Check if your configs are still stored. -```shell -stackit config profile list -``` +2. You can then install the CLI via (this also trusts the STACKIT CLI cask): -6. In case the profiles are gone, import your profiles via: ```shell -$ stackit config profile import -c @default.json --name myProfile +brew install --cask stackitcloud/tap/stackit ``` ### Linux @@ -67,7 +37,7 @@ $ stackit config profile import -c @default.json --name myProfile The STACKIT CLI is available as a [Snap](https://snapcraft.io/stackit), and can be installed via: ```shell -sudo snap install stackit --beta --classic +sudo snap install stackit --classic ``` or via the [Snap Store](https://snapcraft.io/snap-store) for desktop. @@ -130,16 +100,54 @@ asset_filters=["stackit-cli_", "_linux_amd64.tar.gz"] eget stackitcloud/stackit-cli ``` -#### RPM package via dnf, yum and zypper +#### RHEL/Fedora/Rocky/Alma/openSUSE/... (`DNF/YUM/Zypper`) + +The STACKIT CLI can be installed through the [`DNF/YUM`](https://docs.fedoraproject.org/en-US/fedora/f40/system-administrators-guide/package-management/DNF/) / [`Zypper`](https://de.opensuse.org/Zypper) package managers. + +> Requires rpm version 4.15 or newer to support Ed25519 signatures. -The STACKIT CLI is available as [RPM Package](https://github.com/stackitcloud/stackit-cli/releases) and can be installed via dnf, yum and zypper package manager. +> `$basearch` is supported by modern distributions. On older systems that don't expand `$basearch`, replace it in the `baseurl` with your architecture explicitly (for example, `.../rpm/cli/x86_64` or `.../rpm/cli/aarch64`). -Just download the rpm package from the [release page](https://github.com/stackitcloud/stackit-cli/releases) and run the install command like the following: +##### Installation via DNF/YUM + +1. Add the repository: ```shell -dnf install stackitcli.rpm -yum install stackitcli.rpm -zypper install stackitcli.rpm +sudo tee /etc/yum.repos.d/stackit.repo > /dev/null << 'EOF' +[stackit] +name=STACKIT CLI +baseurl=https://packages.stackit.cloud/rpm/cli/$basearch +enabled=1 +gpgcheck=1 +gpgkey=https://packages.stackit.cloud/keys/key.gpg +EOF +``` + +2. Install the CLI: + +```shell +sudo dnf install stackit +``` + +##### Installation via Zypper + +1. Add the repository: + +```shell +sudo tee /etc/zypp/repos.d/stackit.repo > /dev/null << 'EOF' +[stackit] +name=STACKIT CLI +baseurl=https://packages.stackit.cloud/rpm/cli/$basearch +enabled=1 +gpgcheck=1 +gpgkey=https://packages.stackit.cloud/keys/key.gpg +EOF +``` + +2. Install the CLI: + +```shell +sudo zypper install stackit ``` #### Any distribution @@ -150,7 +158,22 @@ Alternatively, you can install via [Homebrew](https://brew.sh/) or refer to one ### Windows -> We are currently working on distributing the CLI on a package manager for Windows. For the moment, please refer to one of the installation methods below. +#### Scoop + +The STACKIT CLI can be installed through the [Scoop](https://scoop.sh/) package manager. + +1. Install Scoop (if not already installed): + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression +``` + +2. Install the CLI: + +```powershell +scoop install stackit +``` ## Manual installation @@ -177,6 +200,24 @@ You can also get the STACKIT CLI by compiling it from source or downloading a pr go run . ``` +### FreeBSD + +The STACKIT CLI can be installed through the [FreeBSD ports or packages](https://docs.freebsd.org/en/books/handbook/ports/). + +To install the port: + +```shell +cd /usr/ports/sysutils/stackit/ && make install clean +``` + +To add the package, run one of these commands: + +```shell +pkg install sysutils/stackit +# OR +pkg install stackit +``` + ### Pre-compiled binary 1. Download the binary corresponding to your operating system and CPU architecture from our [Releases](https://github.com/stackitcloud/stackit-cli/releases) page diff --git a/Makefile b/Makefile index a3b64bb3d..436a40a80 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ ROOT_DIR ?= $(shell git rev-parse --show-toplevel) SCRIPTS_BASE ?= $(ROOT_DIR)/scripts GOLANG_CI_YAML_PATH ?= ${ROOT_DIR}/golang-ci.yaml -GOLANG_CI_ARGS ?= --allow-parallel-runners --timeout=5m --config=${GOLANG_CI_YAML_PATH} +GOLANG_CI_ARGS ?= --allow-parallel-runners --config=${GOLANG_CI_YAML_PATH} # Build build: @@ -9,7 +9,7 @@ build: fmt: @gofmt -s -w . - @go tool goimports -w . + @go tool golangci-lint fmt --config=${GOLANG_CI_YAML_PATH} # Lint lint-golangci-lint: diff --git a/README.md b/README.md index fad966e7b..80e5aee38 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@
-# STACKIT CLI (BETA) +# STACKIT CLI [![Go Report Card](https://goreportcard.com/badge/github.com/stackitcloud/stackit-cli)](https://goreportcard.com/report/github.com/stackitcloud/stackit-cli) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/stackitcloud/stackit-cli) [![GitHub License](https://img.shields.io/github/license/stackitcloud/stackit-cli)](https://www.apache.org/licenses/LICENSE-2.0) -Welcome to the STACKIT CLI, a command-line interface for [STACKIT - The German business cloud](https://www.stackit.de/en). +Welcome to the STACKIT CLI, a command-line interface for [STACKIT - The sovereign cloud for Europe](https://www.stackit.de/en). The STACKIT CLI allows you to manage your STACKIT services and resources as well as perform operations using the command-line or in scripts or automation, such as: @@ -19,7 +19,6 @@ The STACKIT CLI allows you to manage your STACKIT services and resources as well - DNS zones and record-sets - Databases such as PostgreSQL Flex, MongoDB Flex and SQLServer Flex -This CLI is in a BETA state. More services and functionality will be supported soon. Your feedback is appreciated! Feel free to open [GitHub issues](https://github.com/stackitcloud/stackit-cli) to provide feature requests and bug reports. @@ -69,28 +68,34 @@ Help is available for any command by specifying the special flag `--help` (or si Below you can find a list of the STACKIT services already available in the CLI (along with their respective command names) and the ones that are currently planned to be integrated. -| Service | CLI Commands | Status | -| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | -| Authorization | `project`, `organization` | :white_check_mark: | -| DNS | `dns` | :white_check_mark: | -| Infrastructure as a Service (IaaS) | `image`
`key-pair`
`network`
`network-area`
`network-interface`
`public-ip`
`quota`
`security-group`
`server`
`volume` | :white_check_mark:| -| Kubernetes Engine (SKE) | `ske` | :white_check_mark: | -| Load Balancer | `load-balancer` | :white_check_mark: | -| LogMe | `logme` | :white_check_mark: | -| MariaDB | `mariadb` | :white_check_mark: | -| MongoDB Flex | `mongodbflex` | :white_check_mark: | -| Observability | `observability` | :white_check_mark: | -| Object Storage | `object-storage` | :white_check_mark: | -| OpenSearch | `opensearch` | :white_check_mark: | -| PostgreSQL Flex | `postgresflex` | :white_check_mark: | -| RabbitMQ | `rabbitmq` | :white_check_mark: | -| Redis | `redis` | :white_check_mark: | -| Resource Manager | `project` | :white_check_mark: | -| Secrets Manager | `secrets-manager` | :white_check_mark: | -| Server Backup Management | `server backup` | :white_check_mark: | -| Server Command (Run Command) | `server command` | :white_check_mark: | -| Service Account | `service-account` | :white_check_mark: | -| SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | +| Service | CLI Commands | Status | +|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| +| Application Load Balancer | `beta alb` | :white_check_mark: (beta) | +| Authorization | `project`, `organization` | :white_check_mark: | +| DNS | `dns` | :white_check_mark: | +| Edge Cloud | `beta edge-cloud` | :white_check_mark: (beta) | +| Git | `git` | :white_check_mark: | +| Infrastructure as a Service (IaaS) | `affinity-group`
`image`
`key-pair`
`network`
`network-area`
`network-interface`
`public-ip`
`quota`
`security-group`
`server`
`volume` | :white_check_mark: | +| Intake | `beta intake` | :white_check_mark: (beta) | +| Key Management Service (KMS) | `beta kms` | :white_check_mark: (beta) | +| Kubernetes Engine (SKE) | `ske` | :white_check_mark: | +| Load Balancer | `load-balancer` | :white_check_mark: | +| LogMe | `logme` | :white_check_mark: | +| MariaDB | `mariadb` | :white_check_mark: | +| MongoDB Flex | `mongodbflex` | :white_check_mark: | +| Observability | `observability` | :white_check_mark: | +| Object Storage | `object-storage` | :white_check_mark: | +| OpenSearch | `opensearch` | :white_check_mark: | +| PostgreSQL Flex | `postgresflex` | :white_check_mark: | +| RabbitMQ | `rabbitmq` | :white_check_mark: | +| Redis | `redis` | :white_check_mark: | +| Resource Manager | `project` | :white_check_mark: | +| Secrets Manager | `secrets-manager` | :white_check_mark: | +| Server Backup Management | `server backup` | :white_check_mark: | +| Server Command (Run Command) | `server command` | :white_check_mark: | +| Service Account | `service-account` | :white_check_mark: | +| SQLServer Flex | `beta sqlserverflex` | :white_check_mark: (beta) | +| File Storage (SFS) | `beta sfs` | :white_check_mark: (beta) | ## Authentication @@ -187,7 +192,7 @@ If you encounter any issues or have suggestions for improvements, please open an ## Contribute -Your contribution is welcome! For more details on how to contribute, refer to our [contribution guide](./CONTRIBUTION.md). +Your contribution is welcome! For more details on how to contribute, refer to our [contribution guide](./CONTRIBUTING.md). ## Release creation @@ -203,6 +208,6 @@ Apache 2.0 - [STACKIT](https://www.stackit.de/en/) -- [STACKIT Knowledge Base](https://docs.stackit.cloud/stackit/en/knowledge-base-85301704.html) +- [STACKIT Docs](https://docs.stackit.cloud/) - [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) diff --git a/aptly.rb b/aptly.rb deleted file mode 100644 index 8b1f49727..000000000 --- a/aptly.rb +++ /dev/null @@ -1,40 +0,0 @@ -class Aptly < Formula - desc "Swiss army knife for Debian repository management" - homepage "https://www.aptly.info/" - url "https://github.com/aptly-dev/aptly/archive/refs/tags/v1.5.0.tar.gz" - sha256 "07e18ce606feb8c86a1f79f7f5dd724079ac27196faa61a2cefa5b599bbb5bb1" - license "MIT" - head "https://github.com/aptly-dev/aptly.git", branch: "master" - - bottle do - rebuild 2 - sha256 cellar: :any_skip_relocation, arm64_sequoia: "f689184731329b1c22f23af361e31cd8aa6992084434d49281227654281a8f45" - sha256 cellar: :any_skip_relocation, arm64_sonoma: "0d022b595e520ea53e23b1dfceb4a45139e7e2ba735994196135c1f9c1a36d4c" - sha256 cellar: :any_skip_relocation, arm64_ventura: "c6fa91fb368a63d5558b8c287b330845e04f90bd4fe7223e161493b01747c869" - sha256 cellar: :any_skip_relocation, arm64_monterey: "19c0c8c0b35c1c5faa2a71fc0bd088725f5623f465369dcca5b2cea59322714c" - sha256 cellar: :any_skip_relocation, arm64_big_sur: "2314abe4aae7ea53660920d311cacccd168045994e1a9eddf12a381b215c1908" - sha256 cellar: :any_skip_relocation, sonoma: "0f077e265538e235ad867b39edc756180c8a0fba7ac5385ab59b18e827519f4c" - sha256 cellar: :any_skip_relocation, ventura: "d132d06243b93952309f3fbe1970d87cde272ea103cf1829c880c1b8a85a12cb" - sha256 cellar: :any_skip_relocation, monterey: "86111a102d0782a77bab0d48015bd275f120a36964d86f8f613f1a8f73d94664" - sha256 cellar: :any_skip_relocation, big_sur: "d622cfe1d925f0058f583b8bf48b0bdcee36a441f1bcf145040e5f93879f8765" - sha256 cellar: :any_skip_relocation, catalina: "5d9d495ec8215cfade3e856528dfa233496849517813b19a9ba8d60cb72c4751" - sha256 cellar: :any_skip_relocation, x86_64_linux: "bbff5503f74ef5dcaae33846e285ecf1a23c23de1c858760ae1789ef6fc99524" - end - - depends_on "go" => :build - - def install - system "go", "generate" if build.head? - system "go", "build", *std_go_args(ldflags: "-s -w -X main.Version=#{version}") - - bash_completion.install "completion.d/aptly" - end - - test do - assert_match "aptly version:", shell_output("#{bin}/aptly version") - - (testpath/".aptly.conf").write("{}") - result = shell_output("#{bin}/aptly -config='#{testpath}/.aptly.conf' mirror list") - assert_match "No mirrors found, create one with", result - end -end \ No newline at end of file diff --git a/docs/stackit.md b/docs/stackit.md index 3cc35f2f9..e00b8b8ac 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -5,8 +5,7 @@ Manage STACKIT resources using the command line ### Synopsis Manage STACKIT resources using the command line. -This CLI is in a BETA state. -More services and functionality will be supported soon. Your feedback is appreciated! +Your feedback is appreciated! ``` stackit [flags] @@ -18,10 +17,10 @@ stackit [flags] -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously -h, --help Help for "stackit" - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") -v, --version Show "stackit" version ``` @@ -36,8 +35,10 @@ stackit [flags] * [stackit git](./stackit_git.md) - Provides functionality for STACKIT Git * [stackit image](./stackit_image.md) - Manage server images * [stackit key-pair](./stackit_key-pair.md) - Provides functionality for SSH key pairs +* [stackit kms](./stackit_kms.md) - Provides functionality for KMS * [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer * [stackit logme](./stackit_logme.md) - Provides functionality for LogMe +* [stackit logs](./stackit_logs.md) - Provides functionality for Logs * [stackit mariadb](./stackit_mariadb.md) - Provides functionality for MariaDB * [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex * [stackit network](./stackit_network.md) - Provides functionality for networks diff --git a/docs/stackit_affinity-group.md b/docs/stackit_affinity-group.md index 9603fe20f..62422e9ef 100644 --- a/docs/stackit_affinity-group.md +++ b/docs/stackit_affinity-group.md @@ -21,10 +21,10 @@ stackit affinity-group [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_affinity-group_create.md b/docs/stackit_affinity-group_create.md index fb63f39cf..8e42fd586 100644 --- a/docs/stackit_affinity-group_create.md +++ b/docs/stackit_affinity-group_create.md @@ -30,10 +30,10 @@ stackit affinity-group create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_affinity-group_delete.md b/docs/stackit_affinity-group_delete.md index 4baa73768..511f768cd 100644 --- a/docs/stackit_affinity-group_delete.md +++ b/docs/stackit_affinity-group_delete.md @@ -28,10 +28,10 @@ stackit affinity-group delete AFFINITY_GROUP [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_affinity-group_describe.md b/docs/stackit_affinity-group_describe.md index 79276bba0..acee8ff04 100644 --- a/docs/stackit_affinity-group_describe.md +++ b/docs/stackit_affinity-group_describe.md @@ -28,10 +28,10 @@ stackit affinity-group describe AFFINITY_GROUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_affinity-group_list.md b/docs/stackit_affinity-group_list.md index ea96c20ac..439f90684 100644 --- a/docs/stackit_affinity-group_list.md +++ b/docs/stackit_affinity-group_list.md @@ -32,10 +32,10 @@ stackit affinity-group list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_auth.md b/docs/stackit_auth.md index 3f9406c46..708ec5d5a 100644 --- a/docs/stackit_auth.md +++ b/docs/stackit_auth.md @@ -21,10 +21,10 @@ stackit auth [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_auth_activate-service-account.md b/docs/stackit_auth_activate-service-account.md index 3d154ebfb..a38eaccba 100644 --- a/docs/stackit_auth_activate-service-account.md +++ b/docs/stackit_auth_activate-service-account.md @@ -26,6 +26,9 @@ stackit auth activate-service-account [flags] Only print the corresponding access token by using the service account token. This access token can be stored as environment variable (STACKIT_ACCESS_TOKEN) in order to be used for all subsequent commands. $ stackit auth activate-service-account --service-account-token my-service-account-token --only-print-access-token + + Authenticate via Workload Identity Federation (OIDC) and print the short-lived access token. Use --use-oidc to explicitly enable OIDC (takes precedence over STACKIT_USE_OIDC); no service account key file is required. + $ STACKIT_SERVICE_ACCOUNT_EMAIL=ci@sa.stackit.cloud stackit auth activate-service-account --use-oidc --only-print-access-token ``` ### Options @@ -36,6 +39,7 @@ stackit auth activate-service-account [flags] --private-key-path string RSA private key path. It takes precedence over the private key included in the service account key, if present --service-account-key-path string Service account key path --service-account-token string Service account long-lived access token + --use-oidc Use Workload Identity Federation (OIDC). If set, this takes precedence over STACKIT_USE_OIDC ``` ### Options inherited from parent commands @@ -43,10 +47,10 @@ stackit auth activate-service-account [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_auth_get-access-token.md b/docs/stackit_auth_get-access-token.md index cc5218002..5e671612b 100644 --- a/docs/stackit_auth_get-access-token.md +++ b/docs/stackit_auth_get-access-token.md @@ -28,10 +28,10 @@ stackit auth get-access-token [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_auth_login.md b/docs/stackit_auth_login.md index 8b08bc947..232c2cbe3 100644 --- a/docs/stackit_auth_login.md +++ b/docs/stackit_auth_login.md @@ -21,7 +21,9 @@ stackit auth login [flags] ### Options ``` - -h, --help Help for "stackit auth login" + -h, --help Help for "stackit auth login" + --port int The port on which the callback server will listen to. By default, it tries to bind a port between 8000 and 8020. + When a value is specified, it will only try to use the specified port. Valid values are within the range of 8000 to 8020. ``` ### Options inherited from parent commands @@ -29,10 +31,10 @@ stackit auth login [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_auth_logout.md b/docs/stackit_auth_logout.md index 4361a9925..836cc92e7 100644 --- a/docs/stackit_auth_logout.md +++ b/docs/stackit_auth_logout.md @@ -28,10 +28,10 @@ stackit auth logout [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta.md b/docs/stackit_beta.md index b58eb067a..52221ace6 100644 --- a/docs/stackit_beta.md +++ b/docs/stackit_beta.md @@ -32,15 +32,20 @@ stackit beta [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit beta alb](./stackit_beta_alb.md) - Manages application loadbalancers +* [stackit beta cdn](./stackit_beta_cdn.md) - Manage CDN resources +* [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) * [stackit beta sqlserverflex](./stackit_beta_sqlserverflex.md) - Provides functionality for SQLServer Flex +* [stackit beta vpn](./stackit_beta_vpn.md) - Provides functionality for VPN diff --git a/docs/stackit_beta_alb.md b/docs/stackit_beta_alb.md index a4e8d9866..d21262d78 100644 --- a/docs/stackit_beta_alb.md +++ b/docs/stackit_beta_alb.md @@ -21,10 +21,10 @@ stackit beta alb [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_create.md b/docs/stackit_beta_alb_create.md index d33b66ab2..6133a5a7e 100644 --- a/docs/stackit_beta_alb_create.md +++ b/docs/stackit_beta_alb_create.md @@ -29,10 +29,10 @@ stackit beta alb create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_delete.md b/docs/stackit_beta_alb_delete.md index a83567e86..f4298611e 100644 --- a/docs/stackit_beta_alb_delete.md +++ b/docs/stackit_beta_alb_delete.md @@ -28,10 +28,10 @@ stackit beta alb delete LOADBALANCER_NAME_ARG [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_describe.md b/docs/stackit_beta_alb_describe.md index 008ba874c..363446c6a 100644 --- a/docs/stackit_beta_alb_describe.md +++ b/docs/stackit_beta_alb_describe.md @@ -28,10 +28,10 @@ stackit beta alb describe LOADBALANCER_NAME_ARG [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_list.md b/docs/stackit_beta_alb_list.md index 639b541dd..86d133543 100644 --- a/docs/stackit_beta_alb_list.md +++ b/docs/stackit_beta_alb_list.md @@ -32,10 +32,10 @@ stackit beta alb list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_observability-credentials.md b/docs/stackit_beta_alb_observability-credentials.md index f704d001a..457c579c3 100644 --- a/docs/stackit_beta_alb_observability-credentials.md +++ b/docs/stackit_beta_alb_observability-credentials.md @@ -21,10 +21,10 @@ stackit beta alb observability-credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_observability-credentials_add.md b/docs/stackit_beta_alb_observability-credentials_add.md index 9e4e544cc..3325a9e35 100644 --- a/docs/stackit_beta_alb_observability-credentials_add.md +++ b/docs/stackit_beta_alb_observability-credentials_add.md @@ -14,7 +14,7 @@ stackit beta alb observability-credentials add [flags] ``` Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag - $ stackit beta alb observability-credentials add --username xxx --password @./password.txt --display-name yyy + $ stackit beta alb observability-credentials add --username xxx --password @./password.txt --displayname yyy ``` ### Options @@ -22,7 +22,7 @@ stackit beta alb observability-credentials add [flags] ``` -d, --displayname string Displayname for the credentials -h, --help Help for "stackit beta alb observability-credentials add" - --password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt). + --password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. -u, --username string Username for the credentials ``` @@ -31,10 +31,10 @@ stackit beta alb observability-credentials add [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_observability-credentials_delete.md b/docs/stackit_beta_alb_observability-credentials_delete.md index 8cdbdde27..d6aef99f9 100644 --- a/docs/stackit_beta_alb_observability-credentials_delete.md +++ b/docs/stackit_beta_alb_observability-credentials_delete.md @@ -28,10 +28,10 @@ stackit beta alb observability-credentials delete CREDENTIAL_REF [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_observability-credentials_describe.md b/docs/stackit_beta_alb_observability-credentials_describe.md index 9eafb8d93..87399832c 100644 --- a/docs/stackit_beta_alb_observability-credentials_describe.md +++ b/docs/stackit_beta_alb_observability-credentials_describe.md @@ -28,10 +28,10 @@ stackit beta alb observability-credentials describe CREDENTIAL_REF [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_observability-credentials_list.md b/docs/stackit_beta_alb_observability-credentials_list.md index 9a71757b2..0e09fd572 100644 --- a/docs/stackit_beta_alb_observability-credentials_list.md +++ b/docs/stackit_beta_alb_observability-credentials_list.md @@ -35,10 +35,10 @@ stackit beta alb observability-credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_observability-credentials_update.md b/docs/stackit_beta_alb_observability-credentials_update.md index 61994726d..df10c38b4 100644 --- a/docs/stackit_beta_alb_observability-credentials_update.md +++ b/docs/stackit_beta_alb_observability-credentials_update.md @@ -22,7 +22,7 @@ stackit beta alb observability-credentials update CREDENTIAL_REF_ARG [flags] ``` -d, --displayname string Displayname for the credentials -h, --help Help for "stackit beta alb observability-credentials update" - --password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt). + --password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. -u, --username string Username for the credentials ``` @@ -31,10 +31,10 @@ stackit beta alb observability-credentials update CREDENTIAL_REF_ARG [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_plans.md b/docs/stackit_beta_alb_plans.md index 3e46e1185..322f9da33 100644 --- a/docs/stackit_beta_alb_plans.md +++ b/docs/stackit_beta_alb_plans.md @@ -28,10 +28,10 @@ stackit beta alb plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_pool.md b/docs/stackit_beta_alb_pool.md index 1553d06bc..aa1adcdc8 100644 --- a/docs/stackit_beta_alb_pool.md +++ b/docs/stackit_beta_alb_pool.md @@ -21,10 +21,10 @@ stackit beta alb pool [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_pool_update.md b/docs/stackit_beta_alb_pool_update.md index 156452e11..5f54f5aa7 100644 --- a/docs/stackit_beta_alb_pool_update.md +++ b/docs/stackit_beta_alb_pool_update.md @@ -14,7 +14,7 @@ stackit beta alb pool update [flags] ``` Update an application target pool from a configuration file (the name of the pool is read from the file) - $ stackit beta alb update --configuration my-target-pool.json --name my-load-balancer + $ stackit beta alb pool update --configuration my-target-pool.json --name my-load-balancer ``` ### Options @@ -22,7 +22,7 @@ stackit beta alb pool update [flags] ``` -c, --configuration string Filename of the input configuration file -h, --help Help for "stackit beta alb pool update" - -n, --name string Name of the target pool name to update + -n, --name string Name of the application load balancer ``` ### Options inherited from parent commands @@ -30,10 +30,10 @@ stackit beta alb pool update [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_quotas.md b/docs/stackit_beta_alb_quotas.md index 26f9168ce..b02c5ebf4 100644 --- a/docs/stackit_beta_alb_quotas.md +++ b/docs/stackit_beta_alb_quotas.md @@ -28,10 +28,10 @@ stackit beta alb quotas [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_template.md b/docs/stackit_beta_alb_template.md index e914e3024..d7576b11c 100644 --- a/docs/stackit_beta_alb_template.md +++ b/docs/stackit_beta_alb_template.md @@ -23,9 +23,9 @@ stackit beta alb template [flags] ### Options ``` - -f, --format string Defines the output format ('yaml' or 'json'), default is 'json' (default "json") + -f, --format string Defines the output format (one of: [json, yaml]) (default "json") -h, --help Help for "stackit beta alb template" - -t, --type string Defines the output type ('alb' or 'pool'), default is 'alb' (default "alb") + -t, --type string Defines the output type (one of: [alb, pool]) (default "alb") ``` ### Options inherited from parent commands @@ -33,10 +33,10 @@ stackit beta alb template [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_alb_update.md b/docs/stackit_beta_alb_update.md index 36c4a8dd7..e0daa7f31 100644 --- a/docs/stackit_beta_alb_update.md +++ b/docs/stackit_beta_alb_update.md @@ -29,10 +29,10 @@ stackit beta alb update [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_cdn.md b/docs/stackit_beta_cdn.md new file mode 100644 index 000000000..682968d9e --- /dev/null +++ b/docs/stackit_beta_cdn.md @@ -0,0 +1,34 @@ +## stackit beta cdn + +Manage CDN resources + +### Synopsis + +Manage the lifecycle of CDN resources. + +``` +stackit beta cdn [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution.md b/docs/stackit_beta_cdn_distribution.md new file mode 100644 index 000000000..3afc9949a --- /dev/null +++ b/docs/stackit_beta_cdn_distribution.md @@ -0,0 +1,38 @@ +## stackit beta cdn distribution + +Manage CDN distributions + +### Synopsis + +Manage the lifecycle of CDN distributions. + +``` +stackit beta cdn distribution [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta cdn](./stackit_beta_cdn.md) - Manage CDN resources +* [stackit beta cdn distribution create](./stackit_beta_cdn_distribution_create.md) - Create a CDN distribution +* [stackit beta cdn distribution delete](./stackit_beta_cdn_distribution_delete.md) - Delete a CDN distribution +* [stackit beta cdn distribution describe](./stackit_beta_cdn_distribution_describe.md) - Describe a CDN distribution +* [stackit beta cdn distribution list](./stackit_beta_cdn_distribution_list.md) - List CDN distributions +* [stackit beta cdn distribution update](./stackit_beta_cdn_distribution_update.md) - Update a CDN distribution + diff --git a/docs/stackit_beta_cdn_distribution_create.md b/docs/stackit_beta_cdn_distribution_create.md new file mode 100644 index 000000000..85fbc3c2a --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_create.md @@ -0,0 +1,70 @@ +## stackit beta cdn distribution create + +Create a CDN distribution + +### Synopsis + +Create a CDN distribution for a given originUrl in multiple regions. + +``` +stackit beta cdn distribution create [flags] +``` + +### Examples + +``` + Create a CDN distribution with an HTTP backend + $ stackit beta cdn distribution create --http --http-origin-url https://example.com \ +--regions AF,EU + + Create a CDN distribution with an Object Storage backend + $ stackit beta cdn distribution create --bucket --bucket-url https://bucket.example.com \ +--bucket-credentials-access-key-id yyyy --bucket-region EU \ +--regions AF,EU + + Create a CDN distribution passing the password via stdin, take care that there's a '\n' at the end of the input' + $ cat secret.txt | stackit beta cdn distribution create -y --project-id xxx \ +--bucket --bucket-url https://bucket.example.com --bucekt-credentials-access-key-id yyyy --bucket-region EU \ +--regions AF,EU +``` + +### Options + +``` + --blocked-countries strings Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR') + --blocked-ips strings Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1') + --bucket Use Object Storage backend + --bucket-credentials-access-key-id string Access Key ID for Object Storage backend + --bucket-password string Bucket-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. + --bucket-region string Region for Object Storage backend + --bucket-url string Bucket URL for Object Storage backend + --default-cache-duration string ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes) + -h, --help Help for "stackit beta cdn distribution create" + --http Use HTTP backend + --http-geofencing stringArray Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable. + --http-origin-request-headers strings Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers! + --http-origin-url string Origin URL for HTTP backend + --loki Enable Loki log sink for the CDN distribution + --loki-password string Loki-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. + --loki-push-url string Push URL for log sink + --loki-username string Username for log sink + --monthly-limit-bytes int Monthly limit in bytes for the CDN distribution + --optimizer Enable optimizer for the CDN distribution (paid feature). + --regions strings Regions in which content should be cached, multiple values accepted, (multiple of: [EU, US, AF, SA, ASIA]) (default []) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_delete.md b/docs/stackit_beta_cdn_distribution_delete.md new file mode 100644 index 000000000..7b55d2012 --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_delete.md @@ -0,0 +1,40 @@ +## stackit beta cdn distribution delete + +Delete a CDN distribution + +### Synopsis + +Delete a CDN distribution by its ID. + +``` +stackit beta cdn distribution delete [flags] +``` + +### Examples + +``` + Delete a CDN distribution with ID "xxx" + $ stackit beta cdn distribution delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_describe.md b/docs/stackit_beta_cdn_distribution_describe.md new file mode 100644 index 000000000..5f3959207 --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_describe.md @@ -0,0 +1,44 @@ +## stackit beta cdn distribution describe + +Describe a CDN distribution + +### Synopsis + +Describe a CDN distribution by its ID. + +``` +stackit beta cdn distribution describe [flags] +``` + +### Examples + +``` + Get details of a CDN distribution with ID "xxx" + $ stackit beta cdn distribution describe xxx + + Get details of a CDN, including WAF details, for ID "xxx" + $ stackit beta cdn distribution describe xxx --with-waf +``` + +### Options + +``` + -h, --help Help for "stackit beta cdn distribution describe" + --with-waf Include WAF details in the distribution description +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_list.md b/docs/stackit_beta_cdn_distribution_list.md new file mode 100644 index 000000000..9c952b629 --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_list.md @@ -0,0 +1,45 @@ +## stackit beta cdn distribution list + +List CDN distributions + +### Synopsis + +List all CDN distributions in your account. + +``` +stackit beta cdn distribution list [flags] +``` + +### Examples + +``` + List all CDN distributions + $ stackit beta cdn distribution list + + List all CDN distributions sorted by id + $ stackit beta cdn distribution list --sort-by=id +``` + +### Options + +``` + -- int Limit the output to the first n elements + -h, --help Help for "stackit beta cdn distribution list" + --sort-by string Sort entries by a specific field, (one of: [id, createdAt, updatedAt, originUrl, status, originUrlRelated]) (default "createdAt") +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_cdn_distribution_update.md b/docs/stackit_beta_cdn_distribution_update.md new file mode 100644 index 000000000..156741aad --- /dev/null +++ b/docs/stackit_beta_cdn_distribution_update.md @@ -0,0 +1,59 @@ +## stackit beta cdn distribution update + +Update a CDN distribution + +### Synopsis + +Update a CDN distribution by its ID, allowing replacement of its regions. + +``` +stackit beta cdn distribution update [flags] +``` + +### Examples + +``` + update a CDN distribution with ID "xxx" to not use optimizer + $ stackit beta cdn distribution update xxx --optimizer=false +``` + +### Options + +``` + --blocked-countries strings Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR') + --blocked-ips strings Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1') + --bucket Use Object Storage backend + --bucket-credentials-access-key-id string Access Key ID for Object Storage backend + --bucket-password string Bucket-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. + --bucket-region string Region for Object Storage backend + --bucket-url string Bucket URL for Object Storage backend + --default-cache-duration string ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes) + -h, --help Help for "stackit beta cdn distribution update" + --http Use HTTP backend + --http-geofencing stringArray Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable. + --http-origin-request-headers strings Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers! + --http-origin-url string Origin URL for HTTP backend + --loki Enable Loki log sink for the CDN distribution + --loki-password string Loki-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. + --loki-push-url string Push URL for log sink + --loki-username string Username for log sink + --monthly-limit-bytes int Monthly limit in bytes for the CDN distribution + --optimizer Enable optimizer for the CDN distribution (paid feature). + --regions strings Regions in which content should be cached, (multiple of: [EU, US, AF, SA, ASIA]) (default []) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta cdn distribution](./stackit_beta_cdn_distribution.md) - Manage CDN distributions + diff --git a/docs/stackit_beta_edge-cloud.md b/docs/stackit_beta_edge-cloud.md new file mode 100644 index 000000000..399471609 --- /dev/null +++ b/docs/stackit_beta_edge-cloud.md @@ -0,0 +1,37 @@ +## stackit beta edge-cloud + +Provides functionality for edge services. + +### Synopsis + +Provides functionality for STACKIT Edge Cloud (STEC) services. + +``` +stackit beta edge-cloud [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. +* [stackit beta edge-cloud kubeconfig](./stackit_beta_edge-cloud_kubeconfig.md) - Provides functionality for edge kubeconfig. +* [stackit beta edge-cloud plans](./stackit_beta_edge-cloud_plans.md) - Provides functionality for edge service plans. +* [stackit beta edge-cloud token](./stackit_beta_edge-cloud_token.md) - Provides functionality for edge service token. + diff --git a/docs/stackit_beta_edge-cloud_instance.md b/docs/stackit_beta_edge-cloud_instance.md new file mode 100644 index 000000000..8f2098058 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_instance.md @@ -0,0 +1,38 @@ +## stackit beta edge-cloud instance + +Provides functionality for edge instances. + +### Synopsis + +Provides functionality for STACKIT Edge Cloud (STEC) instance management. + +``` +stackit beta edge-cloud instance [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud instance" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. +* [stackit beta edge-cloud instance create](./stackit_beta_edge-cloud_instance_create.md) - Creates an edge instance +* [stackit beta edge-cloud instance delete](./stackit_beta_edge-cloud_instance_delete.md) - Deletes an edge instance +* [stackit beta edge-cloud instance describe](./stackit_beta_edge-cloud_instance_describe.md) - Describes an edge instance +* [stackit beta edge-cloud instance list](./stackit_beta_edge-cloud_instance_list.md) - Lists edge instances +* [stackit beta edge-cloud instance update](./stackit_beta_edge-cloud_instance_update.md) - Updates an edge instance + diff --git a/docs/stackit_beta_edge-cloud_instance_create.md b/docs/stackit_beta_edge-cloud_instance_create.md new file mode 100644 index 000000000..fad42e2b0 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_instance_create.md @@ -0,0 +1,43 @@ +## stackit beta edge-cloud instance create + +Creates an edge instance + +### Synopsis + +Creates a STACKIT Edge Cloud (STEC) instance. The instance will take a moment to become fully functional. + +``` +stackit beta edge-cloud instance create [flags] +``` + +### Examples + +``` + Creates an edge instance with the name "xxx" and plan-id "yyy" + $ stackit beta edge-cloud instance create --name "xxx" --plan-id "yyy" +``` + +### Options + +``` + -d, --description string A user chosen description to distinguish multiple instances. + -h, --help Help for "stackit beta edge-cloud instance create" + -n, --name string The displayed name to distinguish multiple instances. + --plan-id string Service Plan configures the size of the Instance. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. + diff --git a/docs/stackit_beta_edge-cloud_instance_delete.md b/docs/stackit_beta_edge-cloud_instance_delete.md new file mode 100644 index 000000000..20a900f8b --- /dev/null +++ b/docs/stackit_beta_edge-cloud_instance_delete.md @@ -0,0 +1,45 @@ +## stackit beta edge-cloud instance delete + +Deletes an edge instance + +### Synopsis + +Deletes a STACKIT Edge Cloud (STEC) instance. The instance will be deleted permanently. + +``` +stackit beta edge-cloud instance delete [flags] +``` + +### Examples + +``` + Delete an edge instance with id "xxx" + $ stackit beta edge-cloud instance delete --id "xxx" + + Delete an edge instance with name "xxx" + $ stackit beta edge-cloud instance delete --name "xxx" +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud instance delete" + -i, --id string The project-unique identifier of this instance. + -n, --name string The displayed name to distinguish multiple instances. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. + diff --git a/docs/stackit_beta_edge-cloud_instance_describe.md b/docs/stackit_beta_edge-cloud_instance_describe.md new file mode 100644 index 000000000..e7621706b --- /dev/null +++ b/docs/stackit_beta_edge-cloud_instance_describe.md @@ -0,0 +1,45 @@ +## stackit beta edge-cloud instance describe + +Describes an edge instance + +### Synopsis + +Describes a STACKIT Edge Cloud (STEC) instance. + +``` +stackit beta edge-cloud instance describe [flags] +``` + +### Examples + +``` + Describe an edge instance with id "xxx" + $ stackit beta edge-cloud instance describe --id + + Describe an edge instance with name "xxx" + $ stackit beta edge-cloud instance describe --name +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud instance describe" + -i, --id string The project-unique identifier of this instance. + -n, --name string The displayed name to distinguish multiple instances. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. + diff --git a/docs/stackit_beta_edge-cloud_instance_list.md b/docs/stackit_beta_edge-cloud_instance_list.md new file mode 100644 index 000000000..ab6a99842 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_instance_list.md @@ -0,0 +1,44 @@ +## stackit beta edge-cloud instance list + +Lists edge instances + +### Synopsis + +Lists STACKIT Edge Cloud (STEC) instances of a project. + +``` +stackit beta edge-cloud instance list [flags] +``` + +### Examples + +``` + Lists all edge instances of a given project + $ stackit beta edge-cloud instance list + + Lists all edge instances of a given project and limits the output to two instances + $ stackit beta edge-cloud instance list --limit 2 +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud instance list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. + diff --git a/docs/stackit_beta_edge-cloud_instance_update.md b/docs/stackit_beta_edge-cloud_instance_update.md new file mode 100644 index 000000000..935ac1f07 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_instance_update.md @@ -0,0 +1,50 @@ +## stackit beta edge-cloud instance update + +Updates an edge instance + +### Synopsis + +Updates a STACKIT Edge Cloud (STEC) instance. + +``` +stackit beta edge-cloud instance update [flags] +``` + +### Examples + +``` + Updates the description of an edge instance with id "xxx" + $ stackit beta edge-cloud instance update --id "xxx" --description "yyy" + + Updates the plan of an edge instance with name "xxx" + $ stackit beta edge-cloud instance update --name "xxx" --plan-id "yyy" + + Updates the description and plan of an edge instance with id "xxx" + $ stackit beta edge-cloud instance update --id "xxx" --description "yyy" --plan-id "zzz" +``` + +### Options + +``` + -d, --description string A user chosen description to distinguish multiple instances. + -h, --help Help for "stackit beta edge-cloud instance update" + -i, --id string The project-unique identifier of this instance. + -n, --name string The displayed name to distinguish multiple instances. + --plan-id string Service Plan configures the size of the Instance. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud instance](./stackit_beta_edge-cloud_instance.md) - Provides functionality for edge instances. + diff --git a/docs/stackit_beta_edge-cloud_kubeconfig.md b/docs/stackit_beta_edge-cloud_kubeconfig.md new file mode 100644 index 000000000..8f875e404 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_kubeconfig.md @@ -0,0 +1,34 @@ +## stackit beta edge-cloud kubeconfig + +Provides functionality for edge kubeconfig. + +### Synopsis + +Provides functionality for STACKIT Edge Cloud (STEC) kubeconfig management. + +``` +stackit beta edge-cloud kubeconfig [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud kubeconfig" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. +* [stackit beta edge-cloud kubeconfig create](./stackit_beta_edge-cloud_kubeconfig_create.md) - Creates or updates a local kubeconfig file of an edge instance + diff --git a/docs/stackit_beta_edge-cloud_kubeconfig_create.md b/docs/stackit_beta_edge-cloud_kubeconfig_create.md new file mode 100644 index 000000000..8627501f7 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_kubeconfig_create.md @@ -0,0 +1,61 @@ +## stackit beta edge-cloud kubeconfig create + +Creates or updates a local kubeconfig file of an edge instance + +### Synopsis + +Creates or updates a local kubeconfig file of a STACKIT Edge Cloud (STEC) instance. If the config exists in the kubeconfig file, the information will be updated. + +By default, the kubeconfig information of the edge instance is merged into the current kubeconfig file which is determined by Kubernetes client logic. If the kubeconfig file doesn't exist, a new one will be created. +You can override this behavior by specifying a custom filepath with the --filepath flag or disable writing with the --disable-writing flag. +An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 3600 seconds. +Note: the format for the duration is , e.g. 30d for 30 days. You may not combine units. + +``` +stackit beta edge-cloud kubeconfig create [flags] +``` + +### Examples + +``` + Create or update a kubeconfig for the edge instance with id "xxx". If the config exists in the kubeconfig file, the information will be updated. + $ stackit beta edge-cloud kubeconfig create --id "xxx" + + Create or update a kubeconfig for the edge instance with name "xxx" in a custom filepath. + $ stackit beta edge-cloud kubeconfig create --name "xxx" --filepath "yyy" + + Get a kubeconfig for the edge instance with name "xxx" without writing it to a file and format the output as json. + $ stackit beta edge-cloud kubeconfig create --name "xxx" --disable-writing --output-format json + + Create a kubeconfig for the edge instance with id "xxx". This will replace your current kubeconfig file. + $ stackit beta edge-cloud kubeconfig create --id "xxx" --overwrite +``` + +### Options + +``` + --disable-writing Disable writing the kubeconfig to a file. + -e, --expiration string Expiration time for the kubeconfig, e.g. 5d. By default, the token is valid for 1h. + -f, --filepath string Path to the kubeconfig file. A default is chosen by Kubernetes if not set. + -h, --help Help for "stackit beta edge-cloud kubeconfig create" + -i, --id string The project-unique identifier of this instance. + -n, --name string The displayed name to distinguish multiple instances. + --overwrite Force overwrite the kubeconfig file if it exists. + --switch-context Switch to the context in the kubeconfig file to the new context. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud kubeconfig](./stackit_beta_edge-cloud_kubeconfig.md) - Provides functionality for edge kubeconfig. + diff --git a/docs/stackit_beta_edge-cloud_plans.md b/docs/stackit_beta_edge-cloud_plans.md new file mode 100644 index 000000000..cf92d0a42 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_plans.md @@ -0,0 +1,34 @@ +## stackit beta edge-cloud plans + +Provides functionality for edge service plans. + +### Synopsis + +Provides functionality for STACKIT Edge Cloud (STEC) service plan management. + +``` +stackit beta edge-cloud plans [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud plans" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. +* [stackit beta edge-cloud plans list](./stackit_beta_edge-cloud_plans_list.md) - Lists available edge service plans + diff --git a/docs/stackit_beta_edge-cloud_plans_list.md b/docs/stackit_beta_edge-cloud_plans_list.md new file mode 100644 index 000000000..8caba32ba --- /dev/null +++ b/docs/stackit_beta_edge-cloud_plans_list.md @@ -0,0 +1,44 @@ +## stackit beta edge-cloud plans list + +Lists available edge service plans + +### Synopsis + +Lists available STACKIT Edge Cloud (STEC) service plans of a project + +``` +stackit beta edge-cloud plans list [flags] +``` + +### Examples + +``` + Lists all edge plans for a given project + $ stackit beta edge-cloud plan list + + Lists all edge plans for a given project and limits the output to two plans + $ stackit beta edge-cloud plan list --limit 2 +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud plans list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud plans](./stackit_beta_edge-cloud_plans.md) - Provides functionality for edge service plans. + diff --git a/docs/stackit_beta_edge-cloud_token.md b/docs/stackit_beta_edge-cloud_token.md new file mode 100644 index 000000000..a6061a060 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_token.md @@ -0,0 +1,34 @@ +## stackit beta edge-cloud token + +Provides functionality for edge service token. + +### Synopsis + +Provides functionality for STACKIT Edge Cloud (STEC) token management. + +``` +stackit beta edge-cloud token [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta edge-cloud token" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud](./stackit_beta_edge-cloud.md) - Provides functionality for edge services. +* [stackit beta edge-cloud token create](./stackit_beta_edge-cloud_token_create.md) - Creates a token for an edge instance + diff --git a/docs/stackit_beta_edge-cloud_token_create.md b/docs/stackit_beta_edge-cloud_token_create.md new file mode 100644 index 000000000..139fa20d0 --- /dev/null +++ b/docs/stackit_beta_edge-cloud_token_create.md @@ -0,0 +1,49 @@ +## stackit beta edge-cloud token create + +Creates a token for an edge instance + +### Synopsis + +Creates a token for a STACKIT Edge Cloud (STEC) instance. + +An expiration time can be set for the token. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 3600 seconds. +Note: the format for the duration is , e.g. 30d for 30 days. You may not combine units. + +``` +stackit beta edge-cloud token create [flags] +``` + +### Examples + +``` + Create a token for the edge instance with id "xxx". + $ stackit beta edge-cloud token create --id "xxx" + + Create a token for the edge instance with name "xxx". The token will be valid for one day. + $ stackit beta edge-cloud token create --name "xxx" --expiration 1d +``` + +### Options + +``` + -e, --expiration string Expiration time for the kubeconfig, e.g. 5d. By default, the token is valid for 1h. + -h, --help Help for "stackit beta edge-cloud token create" + -i, --id string The project-unique identifier of this instance. + -n, --name string The displayed name to distinguish multiple instances. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta edge-cloud token](./stackit_beta_edge-cloud_token.md) - Provides functionality for edge service token. + diff --git a/docs/stackit_beta_intake.md b/docs/stackit_beta_intake.md new file mode 100644 index 000000000..cf8770f31 --- /dev/null +++ b/docs/stackit_beta_intake.md @@ -0,0 +1,40 @@ +## stackit beta intake + +Provides functionality for intake + +### Synopsis + +Provides functionality for intake. + +``` +stackit beta intake [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta intake" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta intake create](./stackit_beta_intake_create.md) - Creates a new Intake +* [stackit beta intake delete](./stackit_beta_intake_delete.md) - Deletes an Intake +* [stackit beta intake describe](./stackit_beta_intake_describe.md) - Shows details of an Intake +* [stackit beta intake list](./stackit_beta_intake_list.md) - Lists all Intakes +* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners +* [stackit beta intake update](./stackit_beta_intake_update.md) - Updates an Intake +* [stackit beta intake user](./stackit_beta_intake_user.md) - Provides functionality for Intake Users + diff --git a/docs/stackit_beta_intake_create.md b/docs/stackit_beta_intake_create.md new file mode 100644 index 000000000..9057ff00b --- /dev/null +++ b/docs/stackit_beta_intake_create.md @@ -0,0 +1,59 @@ +## stackit beta intake create + +Creates a new Intake + +### Synopsis + +Creates a new Intake. + +``` +stackit beta intake create [flags] +``` + +### Examples + +``` + Create a new Intake with required parameters + $ stackit beta intake create --display-name my-intake --runner-id xxx --catalog-auth-type none --catalog-uri "http://dremio.example.com" --catalog-warehouse "my-warehouse" + + Create a new Intake with a description, labels, and Dremio authentication + $ stackit beta intake create --display-name my-intake --runner-id xxx --description "Production intake" --labels "env=prod,team=billing" --catalog-auth-type "dremio" --catalog-uri "http://dremio.example.com" --catalog-warehouse "my-warehouse" --dremio-token-endpoint "https://auth.dremio.cloud/oauth/token" --dremio-pat "MY_TOKEN" + + Create a new Intake with manual partitioning by a date field + $ stackit beta intake create --display-name my-partitioned-intake --runner-id xxx --catalog-auth-type "none" --catalog-uri "http://dremio.example.com" --catalog-warehouse "my-warehouse" --catalog-partitioning "manual" --catalog-partition-by "day(__intake_ts)" +``` + +### Options + +``` + --catalog-auth-type string Authentication type for the catalog (e.g., 'none', 'dremio') + --catalog-namespace string The namespace to which data shall be written (default: 'intake') + --catalog-partition-by strings List of Iceberg partitioning expressions. Only used when --catalog-partitioning is 'manual' + --catalog-partitioning string The target table's partitioning. One of 'none', 'intake-time', 'manual' + --catalog-table-name string The table name to identify the table in Iceberg + --catalog-uri string The URI to the Iceberg catalog endpoint + --catalog-warehouse string The Iceberg warehouse to connect to + --description string Description + --display-name string Display name + --dremio-pat string Dremio personal access token. Required if auth-type is 'dremio' + --dremio-token-endpoint string Dremio OAuth 2.0 token endpoint URL. Required if auth-type is 'dremio' + -h, --help Help for "stackit beta intake create" + --labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2" (default []) + --runner-id string The UUID of the Intake Runner to use +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake + diff --git a/docs/stackit_beta_intake_delete.md b/docs/stackit_beta_intake_delete.md new file mode 100644 index 000000000..6d6308b54 --- /dev/null +++ b/docs/stackit_beta_intake_delete.md @@ -0,0 +1,40 @@ +## stackit beta intake delete + +Deletes an Intake + +### Synopsis + +Deletes an Intake. + +``` +stackit beta intake delete INTAKE_ID [flags] +``` + +### Examples + +``` + Delete an Intake with ID "xxx" + $ stackit beta intake delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta intake delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake + diff --git a/docs/stackit_beta_intake_describe.md b/docs/stackit_beta_intake_describe.md new file mode 100644 index 000000000..837e3906d --- /dev/null +++ b/docs/stackit_beta_intake_describe.md @@ -0,0 +1,43 @@ +## stackit beta intake describe + +Shows details of an Intake + +### Synopsis + +Shows details of an Intake. + +``` +stackit beta intake describe INTAKE_ID [flags] +``` + +### Examples + +``` + Get details of an Intake with ID "xxx" + $ stackit beta intake describe xxx + + Get details of an Intake with ID "xxx" in JSON format + $ stackit beta intake describe xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta intake describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake + diff --git a/docs/stackit_beta_intake_list.md b/docs/stackit_beta_intake_list.md new file mode 100644 index 000000000..3a0fd8332 --- /dev/null +++ b/docs/stackit_beta_intake_list.md @@ -0,0 +1,47 @@ +## stackit beta intake list + +Lists all Intakes + +### Synopsis + +Lists all Intakes for the current project. + +``` +stackit beta intake list [flags] +``` + +### Examples + +``` + List all Intakes + $ stackit beta intake list + + List all Intakes in JSON format + $ stackit beta intake list --output-format json + + List up to 5 Intakes + $ stackit beta intake list --limit 5 +``` + +### Options + +``` + -h, --help Help for "stackit beta intake list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake + diff --git a/docs/stackit_beta_intake_runner.md b/docs/stackit_beta_intake_runner.md new file mode 100644 index 000000000..222ba1437 --- /dev/null +++ b/docs/stackit_beta_intake_runner.md @@ -0,0 +1,38 @@ +## stackit beta intake runner + +Provides functionality for Intake Runners + +### Synopsis + +Provides functionality for Intake Runners. + +``` +stackit beta intake runner [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta intake runner" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake +* [stackit beta intake runner create](./stackit_beta_intake_runner_create.md) - Creates a new Intake Runner +* [stackit beta intake runner delete](./stackit_beta_intake_runner_delete.md) - Deletes an Intake Runner +* [stackit beta intake runner describe](./stackit_beta_intake_runner_describe.md) - Shows details of an Intake Runner +* [stackit beta intake runner list](./stackit_beta_intake_runner_list.md) - Lists all Intake Runners +* [stackit beta intake runner update](./stackit_beta_intake_runner_update.md) - Updates an Intake Runner + diff --git a/docs/stackit_beta_intake_runner_create.md b/docs/stackit_beta_intake_runner_create.md new file mode 100644 index 000000000..0043ced1d --- /dev/null +++ b/docs/stackit_beta_intake_runner_create.md @@ -0,0 +1,48 @@ +## stackit beta intake runner create + +Creates a new Intake Runner + +### Synopsis + +Creates a new Intake Runner. + +``` +stackit beta intake runner create [flags] +``` + +### Examples + +``` + Create a new Intake Runner with a display name and message capacity limits + $ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000 + + Create a new Intake Runner with a description and labels + $ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000 --description "Main runner for production" --labels="env=prod,team=billing" +``` + +### Options + +``` + --description string Description + --display-name string Display name + -h, --help Help for "stackit beta intake runner create" + --labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2" (default []) + --max-message-size-kib int32 Maximum message size in KiB + --max-messages-per-hour int32 Maximum number of messages per hour +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners + diff --git a/docs/stackit_beta_intake_runner_delete.md b/docs/stackit_beta_intake_runner_delete.md new file mode 100644 index 000000000..ac87d1c08 --- /dev/null +++ b/docs/stackit_beta_intake_runner_delete.md @@ -0,0 +1,44 @@ +## stackit beta intake runner delete + +Deletes an Intake Runner + +### Synopsis + +Deletes an Intake Runner. + +``` +stackit beta intake runner delete RUNNER_ID [flags] +``` + +### Examples + +``` + Delete an Intake Runner with ID "xxx" + $ stackit beta intake runner delete xxx + + Delete an Intake Runner with ID "xxx", along with all associated Intakes and Intake Users that would stop the removal of the Intake + $ stackit beta intake runner delete xxx --force +``` + +### Options + +``` + --force When true, also removes all associated Intakes and Intake Users that would stop the removal of the Intake + -h, --help Help for "stackit beta intake runner delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners + diff --git a/docs/stackit_beta_intake_runner_describe.md b/docs/stackit_beta_intake_runner_describe.md new file mode 100644 index 000000000..234247d43 --- /dev/null +++ b/docs/stackit_beta_intake_runner_describe.md @@ -0,0 +1,43 @@ +## stackit beta intake runner describe + +Shows details of an Intake Runner + +### Synopsis + +Shows details of an Intake Runner. + +``` +stackit beta intake runner describe RUNNER_ID [flags] +``` + +### Examples + +``` + Get details of an Intake Runner with ID "xxx" + $ stackit beta intake runner describe xxx + + Get details of an Intake Runner with ID "xxx" in JSON format + $ stackit beta intake runner describe xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta intake runner describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners + diff --git a/docs/stackit_beta_intake_runner_list.md b/docs/stackit_beta_intake_runner_list.md new file mode 100644 index 000000000..10a5f4d71 --- /dev/null +++ b/docs/stackit_beta_intake_runner_list.md @@ -0,0 +1,47 @@ +## stackit beta intake runner list + +Lists all Intake Runners + +### Synopsis + +Lists all Intake Runners for the current project. + +``` +stackit beta intake runner list [flags] +``` + +### Examples + +``` + List all Intake Runners + $ stackit beta intake runner list + + List all Intake Runners in JSON format + $ stackit beta intake runner list --output-format json + + List up to 5 Intake Runners + $ stackit beta intake runner list --limit 5 +``` + +### Options + +``` + -h, --help Help for "stackit beta intake runner list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners + diff --git a/docs/stackit_beta_intake_runner_update.md b/docs/stackit_beta_intake_runner_update.md new file mode 100644 index 000000000..222c207f4 --- /dev/null +++ b/docs/stackit_beta_intake_runner_update.md @@ -0,0 +1,48 @@ +## stackit beta intake runner update + +Updates an Intake Runner + +### Synopsis + +Updates an Intake Runner. Only the specified fields are updated. + +``` +stackit beta intake runner update RUNNER_ID [flags] +``` + +### Examples + +``` + Update the display name of an Intake Runner with ID "xxx" + $ stackit beta intake runner update xxx --display-name "new-runner-name" + + Update the message capacity limits for an Intake Runner with ID "xxx" + $ stackit beta intake runner update xxx --max-message-size-kib 1000 --max-messages-per-hour 10000 +``` + +### Options + +``` + --description string Description + --display-name string Display name + -h, --help Help for "stackit beta intake runner update" + --labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default []) + --max-message-size-kib int32 Maximum message size in KiB. Note: Overall message capacity cannot be decreased. + --max-messages-per-hour int32 Maximum number of messages per hour. Note: Overall message capacity cannot be decreased. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake runner](./stackit_beta_intake_runner.md) - Provides functionality for Intake Runners + diff --git a/docs/stackit_beta_intake_update.md b/docs/stackit_beta_intake_update.md new file mode 100644 index 000000000..167ff47a3 --- /dev/null +++ b/docs/stackit_beta_intake_update.md @@ -0,0 +1,54 @@ +## stackit beta intake update + +Updates an Intake + +### Synopsis + +Updates an Intake. Only the specified fields are updated. + +``` +stackit beta intake update INTAKE_ID [flags] +``` + +### Examples + +``` + Update the display name of an Intake with ID "xxx" + $ stackit beta intake update xxx --runner-id yyy --display-name new-intake-name + + Update the catalog details for an Intake with ID "xxx" + $ stackit beta intake update xxx --runner-id yyy --catalog-uri "http://new.uri" --catalog-warehouse "new-warehouse" +``` + +### Options + +``` + --catalog-auth-type string Authentication type for the catalog (e.g., 'none', 'dremio') + --catalog-namespace string The namespace to which data shall be written + --catalog-table-name string The table name to identify the table in Iceberg + --catalog-uri string The URI to the Iceberg catalog endpoint + --catalog-warehouse string The Iceberg warehouse to connect to + --description string Description + --display-name string Display name + --dremio-pat string Dremio personal access token + --dremio-token-endpoint string Dremio OAuth 2.0 token endpoint URL + -h, --help Help for "stackit beta intake update" + --labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default []) + --runner-id string The UUID of the Intake Runner to use +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake + diff --git a/docs/stackit_beta_intake_user.md b/docs/stackit_beta_intake_user.md new file mode 100644 index 000000000..229a9e659 --- /dev/null +++ b/docs/stackit_beta_intake_user.md @@ -0,0 +1,38 @@ +## stackit beta intake user + +Provides functionality for Intake Users + +### Synopsis + +Provides functionality for Intake Users. + +``` +stackit beta intake user [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta intake user" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake](./stackit_beta_intake.md) - Provides functionality for intake +* [stackit beta intake user create](./stackit_beta_intake_user_create.md) - Creates a new Intake User +* [stackit beta intake user delete](./stackit_beta_intake_user_delete.md) - Deletes an Intake User +* [stackit beta intake user describe](./stackit_beta_intake_user_describe.md) - Shows details of an Intake User +* [stackit beta intake user list](./stackit_beta_intake_user_list.md) - Lists all Intake Users +* [stackit beta intake user update](./stackit_beta_intake_user_update.md) - Updates an Intake User + diff --git a/docs/stackit_beta_intake_user_create.md b/docs/stackit_beta_intake_user_create.md new file mode 100644 index 000000000..fd7b6a93f --- /dev/null +++ b/docs/stackit_beta_intake_user_create.md @@ -0,0 +1,49 @@ +## stackit beta intake user create + +Creates a new Intake User + +### Synopsis + +Creates a new Intake User for a specific Intake. + +``` +stackit beta intake user create [flags] +``` + +### Examples + +``` + Create a new Intake User with required parameters + $ stackit beta intake user create --display-name intake-user --intake-id xxx --password "SuperSafepass123\!" + + Create a new Intake User for the dead-letter queue with labels + $ stackit beta intake user create --display-name dlq-user --intake-id xxx --password "SuperSafepass123\!" --type dead-letter --labels "env=prod" +``` + +### Options + +``` + --description string Description + --display-name string Display name + -h, --help Help for "stackit beta intake user create" + --intake-id string The UUID of the Intake to associate the user with + --labels stringToString Labels in key=value format, separated by commas (default []) + --password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, number, and special characters (min 12 chars) + --type string Type of user. One of 'intake' (default) or 'dead-letter' (default "intake") +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake user](./stackit_beta_intake_user.md) - Provides functionality for Intake Users + diff --git a/docs/stackit_beta_intake_user_delete.md b/docs/stackit_beta_intake_user_delete.md new file mode 100644 index 000000000..3a748f34d --- /dev/null +++ b/docs/stackit_beta_intake_user_delete.md @@ -0,0 +1,41 @@ +## stackit beta intake user delete + +Deletes an Intake User + +### Synopsis + +Deletes an Intake User. + +``` +stackit beta intake user delete USER_ID [flags] +``` + +### Examples + +``` + Delete an Intake User with ID "xxx" for Intake "yyy" + $ stackit beta intake user delete xxx --intake-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta intake user delete" + --intake-id string Intake ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake user](./stackit_beta_intake_user.md) - Provides functionality for Intake Users + diff --git a/docs/stackit_beta_intake_user_describe.md b/docs/stackit_beta_intake_user_describe.md new file mode 100644 index 000000000..ff7b04f0f --- /dev/null +++ b/docs/stackit_beta_intake_user_describe.md @@ -0,0 +1,44 @@ +## stackit beta intake user describe + +Shows details of an Intake User + +### Synopsis + +Shows details of an Intake User. + +``` +stackit beta intake user describe USER_ID [flags] +``` + +### Examples + +``` + Get details of an Intake User with ID "xxx" for Intake "yyy" + $ stackit beta intake user describe xxx --intake-id yyy + + Get details of an Intake User with ID "xxx" in JSON format + $ stackit beta intake user describe xxx --intake-id yyy --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit beta intake user describe" + --intake-id string Intake ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake user](./stackit_beta_intake_user.md) - Provides functionality for Intake Users + diff --git a/docs/stackit_beta_intake_user_list.md b/docs/stackit_beta_intake_user_list.md new file mode 100644 index 000000000..4ff4a4ad8 --- /dev/null +++ b/docs/stackit_beta_intake_user_list.md @@ -0,0 +1,48 @@ +## stackit beta intake user list + +Lists all Intake Users + +### Synopsis + +Lists all Intake Users for a specific Intake. + +``` +stackit beta intake user list [flags] +``` + +### Examples + +``` + List all users for an Intake + $ stackit beta intake user list --intake-id xxx + + List all users for an Intake in JSON format + $ stackit beta intake user list --intake-id xxx --output-format json + + List up to 5 users for an Intake + $ stackit beta intake user list --intake-id xxx --limit 5 +``` + +### Options + +``` + -h, --help Help for "stackit beta intake user list" + --intake-id string Intake ID + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake user](./stackit_beta_intake_user.md) - Provides functionality for Intake Users + diff --git a/docs/stackit_beta_intake_user_update.md b/docs/stackit_beta_intake_user_update.md new file mode 100644 index 000000000..c33b57d06 --- /dev/null +++ b/docs/stackit_beta_intake_user_update.md @@ -0,0 +1,49 @@ +## stackit beta intake user update + +Updates an Intake User + +### Synopsis + +Updates an Intake User. Only the specified fields are updated. + +``` +stackit beta intake user update USER_ID [flags] +``` + +### Examples + +``` + Update the display name of an Intake User + $ stackit beta intake user update xxx --intake-id yyy --display-name "new-user-name" + + Update the password and description for an Intake User + $ stackit beta intake user update xxx --intake-id yyy --password "NewSecret123\!" --description "Updated description" +``` + +### Options + +``` + --description string Description + --display-name string Display name + -h, --help Help for "stackit beta intake user update" + --intake-id string Intake ID + --labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default []) + --password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, number, and special characters (min 12 chars) + --type string Type of user. One of 'intake' or 'dead-letter' +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta intake user](./stackit_beta_intake_user.md) - Provides functionality for Intake Users + diff --git a/docs/stackit_beta_sfs.md b/docs/stackit_beta_sfs.md new file mode 100644 index 000000000..09ab6238c --- /dev/null +++ b/docs/stackit_beta_sfs.md @@ -0,0 +1,40 @@ +## stackit beta sfs + +Provides functionality for SFS (STACKIT File Storage) + +### Synopsis + +Provides functionality for SFS (STACKIT File Storage). + +``` +stackit beta sfs [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta sfs export-policy](./stackit_beta_sfs_export-policy.md) - Provides functionality for SFS export policies +* [stackit beta sfs performance-class](./stackit_beta_sfs_performance-class.md) - Provides functionality for SFS performance classes +* [stackit beta sfs project-lock](./stackit_beta_sfs_project-lock.md) - Provides functionality for SFS project locks +* [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools +* [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares +* [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots +* [stackit beta sfs snapshot-policy](./stackit_beta_sfs_snapshot-policy.md) - Provides functionality for SFS snapshot policies + diff --git a/docs/stackit_beta_sfs_export-policy.md b/docs/stackit_beta_sfs_export-policy.md new file mode 100644 index 000000000..1ddb33704 --- /dev/null +++ b/docs/stackit_beta_sfs_export-policy.md @@ -0,0 +1,38 @@ +## stackit beta sfs export-policy + +Provides functionality for SFS export policies + +### Synopsis + +Provides functionality for SFS export policies. + +``` +stackit beta sfs export-policy [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs export-policy" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs export-policy create](./stackit_beta_sfs_export-policy_create.md) - Creates an export policy +* [stackit beta sfs export-policy delete](./stackit_beta_sfs_export-policy_delete.md) - Deletes an export policy +* [stackit beta sfs export-policy describe](./stackit_beta_sfs_export-policy_describe.md) - Shows details of an export policy +* [stackit beta sfs export-policy list](./stackit_beta_sfs_export-policy_list.md) - Lists all export policies of a project +* [stackit beta sfs export-policy update](./stackit_beta_sfs_export-policy_update.md) - Updates an export policy + diff --git a/docs/stackit_beta_sfs_export-policy_create.md b/docs/stackit_beta_sfs_export-policy_create.md new file mode 100644 index 000000000..94e9117ff --- /dev/null +++ b/docs/stackit_beta_sfs_export-policy_create.md @@ -0,0 +1,45 @@ +## stackit beta sfs export-policy create + +Creates an export policy + +### Synopsis + +Creates an export policy. + +``` +stackit beta sfs export-policy create [flags] +``` + +### Examples + +``` + Create a new export policy with name "EXPORT_POLICY_NAME" + $ stackit beta sfs export-policy create --name EXPORT_POLICY_NAME + + Create a new export policy with name "EXPORT_POLICY_NAME" and rules from file "./rules.json" + $ stackit beta sfs export-policy create --name EXPORT_POLICY_NAME --rules @./rules.json +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs export-policy create" + --name string Export policy name + --rules string Rules of the export policy (format: json) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs export-policy](./stackit_beta_sfs_export-policy.md) - Provides functionality for SFS export policies + diff --git a/docs/stackit_beta_sfs_export-policy_delete.md b/docs/stackit_beta_sfs_export-policy_delete.md new file mode 100644 index 000000000..3209a00ed --- /dev/null +++ b/docs/stackit_beta_sfs_export-policy_delete.md @@ -0,0 +1,40 @@ +## stackit beta sfs export-policy delete + +Deletes an export policy + +### Synopsis + +Deletes an export policy. + +``` +stackit beta sfs export-policy delete EXPORT_POLICY_ID [flags] +``` + +### Examples + +``` + Delete an export policy with ID "xxx" + $ stackit beta sfs export-policy delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs export-policy delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs export-policy](./stackit_beta_sfs_export-policy.md) - Provides functionality for SFS export policies + diff --git a/docs/stackit_beta_sfs_export-policy_describe.md b/docs/stackit_beta_sfs_export-policy_describe.md new file mode 100644 index 000000000..43f1e4618 --- /dev/null +++ b/docs/stackit_beta_sfs_export-policy_describe.md @@ -0,0 +1,40 @@ +## stackit beta sfs export-policy describe + +Shows details of an export policy + +### Synopsis + +Shows details of an export policy. + +``` +stackit beta sfs export-policy describe EXPORT_POLICY_ID [flags] +``` + +### Examples + +``` + Describe an export policy with ID "xxx" + $ stackit beta sfs export-policy describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs export-policy describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs export-policy](./stackit_beta_sfs_export-policy.md) - Provides functionality for SFS export policies + diff --git a/docs/stackit_beta_sfs_export-policy_list.md b/docs/stackit_beta_sfs_export-policy_list.md new file mode 100644 index 000000000..7d9fb3cd4 --- /dev/null +++ b/docs/stackit_beta_sfs_export-policy_list.md @@ -0,0 +1,44 @@ +## stackit beta sfs export-policy list + +Lists all export policies of a project + +### Synopsis + +Lists all export policies of a project. + +``` +stackit beta sfs export-policy list [flags] +``` + +### Examples + +``` + List all export policies + $ stackit beta sfs export-policy list + + List up to 10 export policies + $ stackit beta sfs export-policy list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs export-policy list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs export-policy](./stackit_beta_sfs_export-policy.md) - Provides functionality for SFS export policies + diff --git a/docs/stackit_beta_sfs_export-policy_update.md b/docs/stackit_beta_sfs_export-policy_update.md new file mode 100644 index 000000000..e7317bc3e --- /dev/null +++ b/docs/stackit_beta_sfs_export-policy_update.md @@ -0,0 +1,45 @@ +## stackit beta sfs export-policy update + +Updates an export policy + +### Synopsis + +Updates an export policy. + +``` +stackit beta sfs export-policy update EXPORT_POLICY_ID [flags] +``` + +### Examples + +``` + Update an export policy with ID "xxx" and with rules from file "./rules.json" + $ stackit beta sfs export-policy update xxx --rules @./rules.json + + Update an export policy with ID "xxx" and remove the rules + $ stackit beta sfs export-policy update XXX --remove-rules +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs export-policy update" + --remove-rules Remove the export policy rules + --rules string Rules of the export policy +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs export-policy](./stackit_beta_sfs_export-policy.md) - Provides functionality for SFS export policies + diff --git a/docs/stackit_beta_sfs_performance-class.md b/docs/stackit_beta_sfs_performance-class.md new file mode 100644 index 000000000..183a5a4af --- /dev/null +++ b/docs/stackit_beta_sfs_performance-class.md @@ -0,0 +1,34 @@ +## stackit beta sfs performance-class + +Provides functionality for SFS performance classes + +### Synopsis + +Provides functionality for SFS performance classes. + +``` +stackit beta sfs performance-class [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs performance-class" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs performance-class list](./stackit_beta_sfs_performance-class_list.md) - Lists all performances classes available + diff --git a/docs/stackit_beta_sfs_performance-class_list.md b/docs/stackit_beta_sfs_performance-class_list.md new file mode 100644 index 000000000..b2289b77a --- /dev/null +++ b/docs/stackit_beta_sfs_performance-class_list.md @@ -0,0 +1,40 @@ +## stackit beta sfs performance-class list + +Lists all performances classes available + +### Synopsis + +Lists all performances classes available. + +``` +stackit beta sfs performance-class list [flags] +``` + +### Examples + +``` + List all performances classes + $ stackit beta sfs performance-class list +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs performance-class list" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs performance-class](./stackit_beta_sfs_performance-class.md) - Provides functionality for SFS performance classes + diff --git a/docs/stackit_beta_sfs_project-lock.md b/docs/stackit_beta_sfs_project-lock.md new file mode 100644 index 000000000..7cf93b099 --- /dev/null +++ b/docs/stackit_beta_sfs_project-lock.md @@ -0,0 +1,36 @@ +## stackit beta sfs project-lock + +Provides functionality for SFS project locks + +### Synopsis + +Provides functionality for SFS project locks. + +``` +stackit beta sfs project-lock [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs project-lock" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs project-lock describe](./stackit_beta_sfs_project-lock_describe.md) - Get lock status for a project +* [stackit beta sfs project-lock lock](./stackit_beta_sfs_project-lock_lock.md) - Enables lock for a project +* [stackit beta sfs project-lock unlock](./stackit_beta_sfs_project-lock_unlock.md) - Clean up lock for a project + diff --git a/docs/stackit_beta_sfs_project-lock_describe.md b/docs/stackit_beta_sfs_project-lock_describe.md new file mode 100644 index 000000000..aac9b0d32 --- /dev/null +++ b/docs/stackit_beta_sfs_project-lock_describe.md @@ -0,0 +1,40 @@ +## stackit beta sfs project-lock describe + +Get lock status for a project + +### Synopsis + +Get lock status for a project. + +``` +stackit beta sfs project-lock describe [flags] +``` + +### Examples + +``` + Get lock status for project + $ stackit beta sfs project-lock describe +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs project-lock describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs project-lock](./stackit_beta_sfs_project-lock.md) - Provides functionality for SFS project locks + diff --git a/docs/stackit_beta_sfs_project-lock_lock.md b/docs/stackit_beta_sfs_project-lock_lock.md new file mode 100644 index 000000000..e2d61b91a --- /dev/null +++ b/docs/stackit_beta_sfs_project-lock_lock.md @@ -0,0 +1,40 @@ +## stackit beta sfs project-lock lock + +Enables lock for a project + +### Synopsis + +Enables lock for a project. Necessary for immutable snapshots and to prevent accidental deletion of resources. + +``` +stackit beta sfs project-lock lock [flags] +``` + +### Examples + +``` + Enable lock for project + $ stackit beta sfs project-lock lock +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs project-lock lock" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs project-lock](./stackit_beta_sfs_project-lock.md) - Provides functionality for SFS project locks + diff --git a/docs/stackit_beta_sfs_project-lock_unlock.md b/docs/stackit_beta_sfs_project-lock_unlock.md new file mode 100644 index 000000000..778204f42 --- /dev/null +++ b/docs/stackit_beta_sfs_project-lock_unlock.md @@ -0,0 +1,40 @@ +## stackit beta sfs project-lock unlock + +Clean up lock for a project + +### Synopsis + +Clean up lock for a project. + +``` +stackit beta sfs project-lock unlock [flags] +``` + +### Examples + +``` + Disable lock for project + $ stackit beta sfs project-lock unlock +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs project-lock unlock" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs project-lock](./stackit_beta_sfs_project-lock.md) - Provides functionality for SFS project locks + diff --git a/docs/stackit_beta_sfs_resource-pool.md b/docs/stackit_beta_sfs_resource-pool.md new file mode 100644 index 000000000..d74e254a0 --- /dev/null +++ b/docs/stackit_beta_sfs_resource-pool.md @@ -0,0 +1,38 @@ +## stackit beta sfs resource-pool + +Provides functionality for SFS resource pools + +### Synopsis + +Provides functionality for SFS resource pools. + +``` +stackit beta sfs resource-pool [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs resource-pool" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs resource-pool create](./stackit_beta_sfs_resource-pool_create.md) - Creates a SFS resource pool +* [stackit beta sfs resource-pool delete](./stackit_beta_sfs_resource-pool_delete.md) - Deletes a SFS resource pool +* [stackit beta sfs resource-pool describe](./stackit_beta_sfs_resource-pool_describe.md) - Shows details of a SFS resource pool +* [stackit beta sfs resource-pool list](./stackit_beta_sfs_resource-pool_list.md) - Lists all SFS resource pools +* [stackit beta sfs resource-pool update](./stackit_beta_sfs_resource-pool_update.md) - Updates a SFS resource pool + diff --git a/docs/stackit_beta_sfs_resource-pool_create.md b/docs/stackit_beta_sfs_resource-pool_create.md new file mode 100644 index 000000000..9c5b95b89 --- /dev/null +++ b/docs/stackit_beta_sfs_resource-pool_create.md @@ -0,0 +1,62 @@ +## stackit beta sfs resource-pool create + +Creates a SFS resource pool + +### Synopsis + +Creates a SFS resource pool. + +The available performance class values can be obtained by running: + $ stackit beta sfs performance-class list + +``` +stackit beta sfs resource-pool create [flags] +``` + +### Examples + +``` + Create a SFS resource pool + $ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 10.88.135.144/28 --performance-class Standard --size 500 --name resource-pool-01 + + Create a SFS resource pool, allow only a single IP which can mount the resource pool + $ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 250.81.87.224/32 --performance-class Standard --size 500 --name resource-pool-01 + + Create a SFS resource pool, allow multiple IP ACL which can mount the resource pool + $ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl "10.88.135.144/28,250.81.87.224/32" --performance-class Standard --size 500 --name resource-pool-01 + + Create a SFS resource pool with visible snapshots + $ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 10.88.135.144/28 --performance-class Standard --size 500 --name resource-pool-01 --snapshots-visible + + Create a SFS resource pool with specific snapshot policy + $ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 10.88.135.144/28 --performance-class Standard --size 500 --name resource-pool-01 --snapshot-policy-id XXX +``` + +### Options + +``` + --availability-zone string Availability zone + -h, --help Help for "stackit beta sfs resource-pool create" + --ip-acl strings List of network addresses in the form
, e.g. 192.168.10.0/24 that can mount the resource pool readonly (default []) + --name string Name + --performance-class string Performance class + --size int32 Size of the pool in Gigabytes + --snapshot-policy-id string Set snapshot policy ID + --snapshots-visible Set snapshots visible and accessible to users +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools + diff --git a/docs/stackit_beta_sfs_resource-pool_delete.md b/docs/stackit_beta_sfs_resource-pool_delete.md new file mode 100644 index 000000000..79e109c8d --- /dev/null +++ b/docs/stackit_beta_sfs_resource-pool_delete.md @@ -0,0 +1,40 @@ +## stackit beta sfs resource-pool delete + +Deletes a SFS resource pool + +### Synopsis + +Deletes a SFS resource pool. + +``` +stackit beta sfs resource-pool delete [flags] +``` + +### Examples + +``` + Delete the SFS resource pool with ID "xxx" + $ stackit beta sfs resource-pool delete xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs resource-pool delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools + diff --git a/docs/stackit_beta_sfs_resource-pool_describe.md b/docs/stackit_beta_sfs_resource-pool_describe.md new file mode 100644 index 000000000..5543271b8 --- /dev/null +++ b/docs/stackit_beta_sfs_resource-pool_describe.md @@ -0,0 +1,40 @@ +## stackit beta sfs resource-pool describe + +Shows details of a SFS resource pool + +### Synopsis + +Shows details of a SFS resource pool. + +``` +stackit beta sfs resource-pool describe [flags] +``` + +### Examples + +``` + Describe the SFS resource pool with ID "xxx" + $ stackit beta sfs resource-pool describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs resource-pool describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools + diff --git a/docs/stackit_beta_sfs_resource-pool_list.md b/docs/stackit_beta_sfs_resource-pool_list.md new file mode 100644 index 000000000..f8f710fb3 --- /dev/null +++ b/docs/stackit_beta_sfs_resource-pool_list.md @@ -0,0 +1,47 @@ +## stackit beta sfs resource-pool list + +Lists all SFS resource pools + +### Synopsis + +Lists all SFS resource pools. + +``` +stackit beta sfs resource-pool list [flags] +``` + +### Examples + +``` + List all SFS resource pools + $ stackit beta sfs resource-pool list + + List all SFS resource pools for another region than the default one + $ stackit beta sfs resource-pool list --region eu01 + + List up to 10 SFS resource pools + $ stackit beta sfs resource-pool list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs resource-pool list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools + diff --git a/docs/stackit_beta_sfs_resource-pool_update.md b/docs/stackit_beta_sfs_resource-pool_update.md new file mode 100644 index 000000000..a6e293e82 --- /dev/null +++ b/docs/stackit_beta_sfs_resource-pool_update.md @@ -0,0 +1,60 @@ +## stackit beta sfs resource-pool update + +Updates a SFS resource pool + +### Synopsis + +Updates a SFS resource pool. + +The available performance class values can be obtained by running: + $ stackit beta sfs performance-class list + +``` +stackit beta sfs resource-pool update [flags] +``` + +### Examples + +``` + Update the SFS resource pool with ID "xxx" + $ stackit beta sfs resource-pool update xxx --ip-acl 10.88.135.144/28 --performance-class Standard --size 5 + + Update the SFS resource pool with ID "xxx", allow only a single IP which can mount the resource pool + $ stackit beta sfs resource-pool update xxx --ip-acl 250.81.87.224/32 --performance-class Standard --size 5 + + Update the SFS resource pool with ID "xxx", allow multiple IP ACL which can mount the resource pool + $ stackit beta sfs resource-pool update xxx --ip-acl "10.88.135.144/28,250.81.87.224/32" --performance-class Standard --size 5 + + Update the SFS resource pool with ID "xxx", set snapshots visible to false + $ stackit beta sfs resource-pool update xxx --snapshots-visible=false + + Update the SFS resource pool with ID "xxx" to set snapshot policy id to "YYY" + $ stackit beta sfs resource-pool update xxx --snapshot-policy-id YYY +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs resource-pool update" + --ip-acl strings List of network addresses in the form
, e.g. 192.168.10.0/24 that can mount the resource pool readonly (default []) + --performance-class string Performance class + --size int32 Size of the pool in Gigabytes + --snapshot-policy-id string Set snapshot policy ID + --snapshots-visible Set snapshots visible and accessible to users +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs resource-pool](./stackit_beta_sfs_resource-pool.md) - Provides functionality for SFS resource pools + diff --git a/docs/stackit_beta_sfs_share.md b/docs/stackit_beta_sfs_share.md new file mode 100644 index 000000000..7c2f4dd72 --- /dev/null +++ b/docs/stackit_beta_sfs_share.md @@ -0,0 +1,38 @@ +## stackit beta sfs share + +Provides functionality for SFS shares + +### Synopsis + +Provides functionality for SFS shares. + +``` +stackit beta sfs share [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs share" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs share create](./stackit_beta_sfs_share_create.md) - Creates a share +* [stackit beta sfs share delete](./stackit_beta_sfs_share_delete.md) - Deletes a share +* [stackit beta sfs share describe](./stackit_beta_sfs_share_describe.md) - Shows details of a shares +* [stackit beta sfs share list](./stackit_beta_sfs_share_list.md) - Lists all shares of a resource pool +* [stackit beta sfs share update](./stackit_beta_sfs_share_update.md) - Updates a share + diff --git a/docs/stackit_beta_sfs_share_create.md b/docs/stackit_beta_sfs_share_create.md new file mode 100644 index 000000000..8e06f59fe --- /dev/null +++ b/docs/stackit_beta_sfs_share_create.md @@ -0,0 +1,47 @@ +## stackit beta sfs share create + +Creates a share + +### Synopsis + +Creates a share. + +``` +stackit beta sfs share create [flags] +``` + +### Examples + +``` + Create a share in a resource pool with ID "xxx", name "yyy" and no space hard limit + $ stackit beta sfs share create --resource-pool-id xxx --name yyy --hard-limit 0 + + Create a share in a resource pool with ID "xxx", name "yyy" and export policy with name "zzz" + $ stackit beta sfs share create --resource-pool-id xxx --name yyy --export-policy-name zzz --hard-limit 0 +``` + +### Options + +``` + --export-policy-name string The export policy the share is assigned to + --hard-limit int32 The space hard limit for the share + -h, --help Help for "stackit beta sfs share create" + --name string Share name + --resource-pool-id string The resource pool the share is assigned to +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares + diff --git a/docs/stackit_beta_sfs_share_delete.md b/docs/stackit_beta_sfs_share_delete.md new file mode 100644 index 000000000..2008d2343 --- /dev/null +++ b/docs/stackit_beta_sfs_share_delete.md @@ -0,0 +1,41 @@ +## stackit beta sfs share delete + +Deletes a share + +### Synopsis + +Deletes a share. + +``` +stackit beta sfs share delete SHARE_ID [flags] +``` + +### Examples + +``` + Delete a share with ID "xxx" from a resource pool with ID "yyy" + $ stackit beta sfs share delete xxx --resource-pool-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs share delete" + --resource-pool-id string The resource pool the share is assigned to +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares + diff --git a/docs/stackit_beta_sfs_share_describe.md b/docs/stackit_beta_sfs_share_describe.md new file mode 100644 index 000000000..8c4a982a8 --- /dev/null +++ b/docs/stackit_beta_sfs_share_describe.md @@ -0,0 +1,41 @@ +## stackit beta sfs share describe + +Shows details of a shares + +### Synopsis + +Shows details of a shares. + +``` +stackit beta sfs share describe SHARE_ID [flags] +``` + +### Examples + +``` + Describe a shares with ID "xxx" from resource pool with ID "yyy" + $ stackit beta sfs share describe xxx --resource-pool-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs share describe" + --resource-pool-id string The resource pool the share is assigned to +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares + diff --git a/docs/stackit_beta_sfs_share_list.md b/docs/stackit_beta_sfs_share_list.md new file mode 100644 index 000000000..d547e5f29 --- /dev/null +++ b/docs/stackit_beta_sfs_share_list.md @@ -0,0 +1,45 @@ +## stackit beta sfs share list + +Lists all shares of a resource pool + +### Synopsis + +Lists all shares of a resource pool. + +``` +stackit beta sfs share list [flags] +``` + +### Examples + +``` + List all shares from resource pool with ID "xxx" + $ stackit beta sfs share list --resource-pool-id xxx + + List up to 10 shares from resource pool with ID "xxx" + $ stackit beta sfs share list --resource-pool-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs share list" + --limit int Maximum number of entries to list + --resource-pool-id string The resource pool the share is assigned to +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares + diff --git a/docs/stackit_beta_sfs_share_update.md b/docs/stackit_beta_sfs_share_update.md new file mode 100644 index 000000000..b07168083 --- /dev/null +++ b/docs/stackit_beta_sfs_share_update.md @@ -0,0 +1,46 @@ +## stackit beta sfs share update + +Updates a share + +### Synopsis + +Updates a share. + +``` +stackit beta sfs share update SHARE_ID [flags] +``` + +### Examples + +``` + Update share with ID "xxx" with new export-policy-name "yyy" in resource-pool "zzz" + $ stackit beta sfs share update xxx --export-policy-name yyy --resource-pool-id zzz + + Update share with ID "xxx" with new space hard limit "50" in resource-pool "yyy" + $ stackit beta sfs share update xxx --hard-limit 50 --resource-pool-id yyy +``` + +### Options + +``` + --export-policy-name string The export policy the share is assigned to + --hard-limit int32 The space hard limit for the share + -h, --help Help for "stackit beta sfs share update" + --resource-pool-id string The resource pool the share is assigned to +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs share](./stackit_beta_sfs_share.md) - Provides functionality for SFS shares + diff --git a/docs/stackit_beta_sfs_snapshot-policy.md b/docs/stackit_beta_sfs_snapshot-policy.md new file mode 100644 index 000000000..dbb461462 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot-policy.md @@ -0,0 +1,35 @@ +## stackit beta sfs snapshot-policy + +Provides functionality for SFS snapshot policies + +### Synopsis + +Provides functionality for SFS snapshot policies. + +``` +stackit beta sfs snapshot-policy [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot-policy" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs snapshot-policy describe](./stackit_beta_sfs_snapshot-policy_describe.md) - Shows details of a snapshot policy +* [stackit beta sfs snapshot-policy list](./stackit_beta_sfs_snapshot-policy_list.md) - Lists all snapshot policies of a project + diff --git a/docs/stackit_beta_sfs_snapshot-policy_describe.md b/docs/stackit_beta_sfs_snapshot-policy_describe.md new file mode 100644 index 000000000..0354fe982 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot-policy_describe.md @@ -0,0 +1,40 @@ +## stackit beta sfs snapshot-policy describe + +Shows details of a snapshot policy + +### Synopsis + +Shows details of a snapshot policy. + +``` +stackit beta sfs snapshot-policy describe SNAPSHOT_POLICY_ID [flags] +``` + +### Examples + +``` + Describe a snapshot policy with ID "xxx" + $ stackit beta sfs snapshot-policy describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot-policy describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot-policy](./stackit_beta_sfs_snapshot-policy.md) - Provides functionality for SFS snapshot policies + diff --git a/docs/stackit_beta_sfs_snapshot-policy_list.md b/docs/stackit_beta_sfs_snapshot-policy_list.md new file mode 100644 index 000000000..474ee9a91 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot-policy_list.md @@ -0,0 +1,51 @@ +## stackit beta sfs snapshot-policy list + +Lists all snapshot policies of a project + +### Synopsis + +Lists all snapshot policies of a project. + +``` +stackit beta sfs snapshot-policy list [flags] +``` + +### Examples + +``` + List all snapshot policies + $ stackit beta sfs snapshot-policy list + + List only mutable snapshot policies + $ stackit beta sfs snapshot-policy list --immutable mutable-only + + List only immutable snapshot policies + $ stackit beta sfs snapshot-policy list --immutable immutable-only + + List up to 10 snapshot policies + $ stackit beta sfs snapshot-policy list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot-policy list" + --immutable string Immutable snapshot policy, (one of: [all, immutable-only, mutable-only]) (default "all") + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot-policy](./stackit_beta_sfs_snapshot-policy.md) - Provides functionality for SFS snapshot policies + diff --git a/docs/stackit_beta_sfs_snapshot.md b/docs/stackit_beta_sfs_snapshot.md new file mode 100644 index 000000000..f9f5cc3e7 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot.md @@ -0,0 +1,38 @@ +## stackit beta sfs snapshot + +Provides functionality for SFS snapshots + +### Synopsis + +Provides functionality for SFS snapshots. + +``` +stackit beta sfs snapshot [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs](./stackit_beta_sfs.md) - Provides functionality for SFS (STACKIT File Storage) +* [stackit beta sfs snapshot create](./stackit_beta_sfs_snapshot_create.md) - Creates a new snapshot of a resource pool +* [stackit beta sfs snapshot delete](./stackit_beta_sfs_snapshot_delete.md) - Deletes a snapshot +* [stackit beta sfs snapshot describe](./stackit_beta_sfs_snapshot_describe.md) - Shows details of a snapshot +* [stackit beta sfs snapshot list](./stackit_beta_sfs_snapshot_list.md) - Lists all snapshots of a resource pool +* [stackit beta sfs snapshot update](./stackit_beta_sfs_snapshot_update.md) - Updates a new snapshot of a resource pool + diff --git a/docs/stackit_beta_sfs_snapshot_create.md b/docs/stackit_beta_sfs_snapshot_create.md new file mode 100644 index 000000000..a9dfb5dd0 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot_create.md @@ -0,0 +1,50 @@ +## stackit beta sfs snapshot create + +Creates a new snapshot of a resource pool + +### Synopsis + +Creates a new snapshot of a resource pool. + +``` +stackit beta sfs snapshot create [flags] +``` + +### Examples + +``` + Create a new snapshot with name "snapshot-name" of a resource pool with ID "xxx" + $ stackit beta sfs snapshot create --name snapshot-name --resource-pool-id xxx + + Create a new snapshot with name "snapshot-name" and comment "snapshot-comment" of a resource pool with ID "xxx" + $ stackit beta sfs snapshot create --name snapshot-name --resource-pool-id xxx --comment "snapshot-comment" + + Create a new snapshot with name "snapshot-name" and snaplock retention hours "24" of a resource pool with ID "xxx" + $ stackit beta sfs snapshot create --name snapshot-name --resource-pool-id xxx --snaplock-retention-hours 24 +``` + +### Options + +``` + --comment string A comment to add more information to the snapshot + -h, --help Help for "stackit beta sfs snapshot create" + --name string Snapshot name + --resource-pool-id string The resource pool from which the snapshot should be created + --snaplock-retention-hours int32 Retention hours for the snaplock +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots + diff --git a/docs/stackit_beta_sfs_snapshot_delete.md b/docs/stackit_beta_sfs_snapshot_delete.md new file mode 100644 index 000000000..865a107fa --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot_delete.md @@ -0,0 +1,41 @@ +## stackit beta sfs snapshot delete + +Deletes a snapshot + +### Synopsis + +Deletes a snapshot. + +``` +stackit beta sfs snapshot delete SNAPSHOT_NAME [flags] +``` + +### Examples + +``` + Delete a snapshot with "SNAPSHOT_NAME" from resource pool with ID "yyy" + $ stackit beta sfs snapshot delete SNAPSHOT_NAME --resource-pool-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot delete" + --resource-pool-id string The resource pool from which the snapshot should be created +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots + diff --git a/docs/stackit_beta_sfs_snapshot_describe.md b/docs/stackit_beta_sfs_snapshot_describe.md new file mode 100644 index 000000000..2cfeaf8be --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot_describe.md @@ -0,0 +1,41 @@ +## stackit beta sfs snapshot describe + +Shows details of a snapshot + +### Synopsis + +Shows details of a snapshot. + +``` +stackit beta sfs snapshot describe SNAPSHOT_NAME [flags] +``` + +### Examples + +``` + Describe a snapshot with "SNAPSHOT_NAME" from resource pool with ID "yyy" + stackit beta sfs snapshot describe SNAPSHOT_NAME --resource-pool-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot describe" + --resource-pool-id string The resource pool from which the snapshot should be created +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots + diff --git a/docs/stackit_beta_sfs_snapshot_list.md b/docs/stackit_beta_sfs_snapshot_list.md new file mode 100644 index 000000000..99fd96c39 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot_list.md @@ -0,0 +1,42 @@ +## stackit beta sfs snapshot list + +Lists all snapshots of a resource pool + +### Synopsis + +Lists all snapshots of a resource pool. + +``` +stackit beta sfs snapshot list [flags] +``` + +### Examples + +``` + List all snapshots of a resource pool with ID "xxx" + $ stackit beta sfs snapshot list --resource-pool-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit beta sfs snapshot list" + --limit int Number of snapshots to list + --resource-pool-id string The resource pool from which the snapshot should be created +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots + diff --git a/docs/stackit_beta_sfs_snapshot_update.md b/docs/stackit_beta_sfs_snapshot_update.md new file mode 100644 index 000000000..a325cb178 --- /dev/null +++ b/docs/stackit_beta_sfs_snapshot_update.md @@ -0,0 +1,46 @@ +## stackit beta sfs snapshot update + +Updates a new snapshot of a resource pool + +### Synopsis + +Updates a new snapshot of a resource pool. + +``` +stackit beta sfs snapshot update SNAPSHOT_NAME [flags] +``` + +### Examples + +``` + Updates the name of a snapshot with name "snapshot-name" of a resource pool with ID "xxx" + $ stackit beta sfs snapshot update snapshot-name --resource-pool-id xxx --name new-snapshot-name + + Updates the comment of a snapshot with name "snapshot-name" of a resource pool with ID "xxx" + $ stackit beta sfs snapshot update snapshot-name --resource-pool-id xxx --comment "snapshot-comment" +``` + +### Options + +``` + --comment string A comment to add more information to the snapshot + -h, --help Help for "stackit beta sfs snapshot update" + --name string Snapshot name + --resource-pool-id string The resource pool from which the snapshot should be updated +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta sfs snapshot](./stackit_beta_sfs_snapshot.md) - Provides functionality for SFS snapshots + diff --git a/docs/stackit_beta_sqlserverflex.md b/docs/stackit_beta_sqlserverflex.md index 1e630a067..88f836410 100644 --- a/docs/stackit_beta_sqlserverflex.md +++ b/docs/stackit_beta_sqlserverflex.md @@ -21,10 +21,10 @@ stackit beta sqlserverflex [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_database.md b/docs/stackit_beta_sqlserverflex_database.md index 19a1d9052..a0cd73855 100644 --- a/docs/stackit_beta_sqlserverflex_database.md +++ b/docs/stackit_beta_sqlserverflex_database.md @@ -21,10 +21,10 @@ stackit beta sqlserverflex database [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_database_create.md b/docs/stackit_beta_sqlserverflex_database_create.md index a34712040..ea1e13797 100644 --- a/docs/stackit_beta_sqlserverflex_database_create.md +++ b/docs/stackit_beta_sqlserverflex_database_create.md @@ -31,10 +31,10 @@ stackit beta sqlserverflex database create DATABASE_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_database_delete.md b/docs/stackit_beta_sqlserverflex_database_delete.md index ebd5ab0a1..343c22956 100644 --- a/docs/stackit_beta_sqlserverflex_database_delete.md +++ b/docs/stackit_beta_sqlserverflex_database_delete.md @@ -30,10 +30,10 @@ stackit beta sqlserverflex database delete DATABASE_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_database_describe.md b/docs/stackit_beta_sqlserverflex_database_describe.md index 290284e23..07262c6e1 100644 --- a/docs/stackit_beta_sqlserverflex_database_describe.md +++ b/docs/stackit_beta_sqlserverflex_database_describe.md @@ -32,10 +32,10 @@ stackit beta sqlserverflex database describe DATABASE_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_database_list.md b/docs/stackit_beta_sqlserverflex_database_list.md index 73797c240..044800699 100644 --- a/docs/stackit_beta_sqlserverflex_database_list.md +++ b/docs/stackit_beta_sqlserverflex_database_list.md @@ -36,10 +36,10 @@ stackit beta sqlserverflex database list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_instance.md b/docs/stackit_beta_sqlserverflex_instance.md index 5ab816add..227b3a487 100644 --- a/docs/stackit_beta_sqlserverflex_instance.md +++ b/docs/stackit_beta_sqlserverflex_instance.md @@ -21,10 +21,10 @@ stackit beta sqlserverflex instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_instance_create.md b/docs/stackit_beta_sqlserverflex_instance_create.md index 6e8dc16a0..002538b2f 100644 --- a/docs/stackit_beta_sqlserverflex_instance_create.md +++ b/docs/stackit_beta_sqlserverflex_instance_create.md @@ -21,7 +21,7 @@ stackit beta sqlserverflex instance create [flags] $ stackit beta sqlserverflex instance create --name my-instance --flavor-id xxx Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values - $ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24 + $ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24 ``` ### Options @@ -29,12 +29,12 @@ stackit beta sqlserverflex instance create [flags] ``` --acl strings The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc. (default []) --backup-schedule string Backup schedule - --cpu int Number of CPUs + --cpu int32 Number of CPUs --edition string Edition of the SQLServer instance --flavor-id string ID of the flavor -h, --help Help for "stackit beta sqlserverflex instance create" -n, --name string Instance name - --ram int Amount of RAM (in GB) + --ram int32 Amount of RAM (in GB) --retention-days int The days for how long the backup files should be stored before being cleaned up --storage-class string Storage class --storage-size int Storage size (in GB) @@ -46,10 +46,10 @@ stackit beta sqlserverflex instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_instance_delete.md b/docs/stackit_beta_sqlserverflex_instance_delete.md index 548bfb952..1e4d05649 100644 --- a/docs/stackit_beta_sqlserverflex_instance_delete.md +++ b/docs/stackit_beta_sqlserverflex_instance_delete.md @@ -28,10 +28,10 @@ stackit beta sqlserverflex instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_instance_describe.md b/docs/stackit_beta_sqlserverflex_instance_describe.md index 42e6a2e3d..5f1ab6ed4 100644 --- a/docs/stackit_beta_sqlserverflex_instance_describe.md +++ b/docs/stackit_beta_sqlserverflex_instance_describe.md @@ -31,10 +31,10 @@ stackit beta sqlserverflex instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_instance_list.md b/docs/stackit_beta_sqlserverflex_instance_list.md index 133a1836d..c6ffaaaf1 100644 --- a/docs/stackit_beta_sqlserverflex_instance_list.md +++ b/docs/stackit_beta_sqlserverflex_instance_list.md @@ -35,10 +35,10 @@ stackit beta sqlserverflex instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_instance_update.md b/docs/stackit_beta_sqlserverflex_instance_update.md index 740628789..b4d901af3 100644 --- a/docs/stackit_beta_sqlserverflex_instance_update.md +++ b/docs/stackit_beta_sqlserverflex_instance_update.md @@ -25,11 +25,11 @@ stackit beta sqlserverflex instance update INSTANCE_ID [flags] ``` --acl strings Lists of IP networks in CIDR notation which are allowed to access this instance (default []) --backup-schedule string Backup schedule - --cpu int Number of CPUs + --cpu int32 Number of CPUs --flavor-id string ID of the flavor -h, --help Help for "stackit beta sqlserverflex instance update" -n, --name string Instance name - --ram int Amount of RAM (in GB) + --ram int32 Amount of RAM (in GB) --version string Version ``` @@ -38,10 +38,10 @@ stackit beta sqlserverflex instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_options.md b/docs/stackit_beta_sqlserverflex_options.md index b7035249e..8caf2ad97 100644 --- a/docs/stackit_beta_sqlserverflex_options.md +++ b/docs/stackit_beta_sqlserverflex_options.md @@ -46,10 +46,10 @@ stackit beta sqlserverflex options [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_user.md b/docs/stackit_beta_sqlserverflex_user.md index b922dba2a..d0c71e9d7 100644 --- a/docs/stackit_beta_sqlserverflex_user.md +++ b/docs/stackit_beta_sqlserverflex_user.md @@ -21,10 +21,10 @@ stackit beta sqlserverflex user [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_user_create.md b/docs/stackit_beta_sqlserverflex_user_create.md index f1cf9dbbe..5ddc1ad26 100644 --- a/docs/stackit_beta_sqlserverflex_user_create.md +++ b/docs/stackit_beta_sqlserverflex_user_create.md @@ -9,7 +9,7 @@ Creates a SQLServer Flex user for an instance. The password is only visible upon creation and cannot be retrieved later. Alternatively, you can reset the password and access the new one by running: $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID -Please refer to https://docs.stackit.cloud/stackit/en/creating-logins-and-users-in-sqlserver-flex-instances-210862358.html for additional information. +Please refer to https://docs.stackit.cloud/products/databases/sqlserver-flex/how-tos/create-logins-and-users-in-sqlserver-flex-instances/ for additional information. The allowed user roles for your instance can be obtained by running: $ stackit beta sqlserverflex options --user-roles --instance-id INSTANCE_ID @@ -42,10 +42,10 @@ stackit beta sqlserverflex user create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_user_delete.md b/docs/stackit_beta_sqlserverflex_user_delete.md index 9ec1d1581..78a9ade81 100644 --- a/docs/stackit_beta_sqlserverflex_user_delete.md +++ b/docs/stackit_beta_sqlserverflex_user_delete.md @@ -30,10 +30,10 @@ stackit beta sqlserverflex user delete USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_user_describe.md b/docs/stackit_beta_sqlserverflex_user_describe.md index b4499f2fa..b62e40039 100644 --- a/docs/stackit_beta_sqlserverflex_user_describe.md +++ b/docs/stackit_beta_sqlserverflex_user_describe.md @@ -34,10 +34,10 @@ stackit beta sqlserverflex user describe USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_user_list.md b/docs/stackit_beta_sqlserverflex_user_list.md index 5a7b79d61..616cd0a74 100644 --- a/docs/stackit_beta_sqlserverflex_user_list.md +++ b/docs/stackit_beta_sqlserverflex_user_list.md @@ -36,10 +36,10 @@ stackit beta sqlserverflex user list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_sqlserverflex_user_reset-password.md b/docs/stackit_beta_sqlserverflex_user_reset-password.md index 210e885bc..feb99fd6a 100644 --- a/docs/stackit_beta_sqlserverflex_user_reset-password.md +++ b/docs/stackit_beta_sqlserverflex_user_reset-password.md @@ -30,10 +30,10 @@ stackit beta sqlserverflex user reset-password USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_beta_vpn.md b/docs/stackit_beta_vpn.md new file mode 100644 index 000000000..861daa4dd --- /dev/null +++ b/docs/stackit_beta_vpn.md @@ -0,0 +1,34 @@ +## stackit beta vpn + +Provides functionality for VPN + +### Synopsis + +Provides functionality for VPN. + +``` +stackit beta vpn [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta](./stackit_beta.md) - Contains beta STACKIT CLI commands +* [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections + diff --git a/docs/stackit_beta_vpn_connection.md b/docs/stackit_beta_vpn_connection.md new file mode 100644 index 000000000..2385727d5 --- /dev/null +++ b/docs/stackit_beta_vpn_connection.md @@ -0,0 +1,38 @@ +## stackit beta vpn connection + +Provides functionality for VPN connections + +### Synopsis + +Provides functionality for VPN connections. + +``` +stackit beta vpn connection [flags] +``` + +### Options + +``` + -h, --help Help for "stackit beta vpn connection" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn](./stackit_beta_vpn.md) - Provides functionality for VPN +* [stackit beta vpn connection create](./stackit_beta_vpn_connection_create.md) - Creates a VPN connection +* [stackit beta vpn connection delete](./stackit_beta_vpn_connection_delete.md) - Deletes a VPN connection +* [stackit beta vpn connection describe](./stackit_beta_vpn_connection_describe.md) - Shows details of a VPN connection +* [stackit beta vpn connection list](./stackit_beta_vpn_connection_list.md) - Lists all VPN connections of a gateway +* [stackit beta vpn connection status](./stackit_beta_vpn_connection_status.md) - Shows the status of a VPN connection + diff --git a/docs/stackit_beta_vpn_connection_create.md b/docs/stackit_beta_vpn_connection_create.md new file mode 100644 index 000000000..221fbb822 --- /dev/null +++ b/docs/stackit_beta_vpn_connection_create.md @@ -0,0 +1,94 @@ +## stackit beta vpn connection create + +Creates a VPN connection + +### Synopsis + +Creates a VPN connection. + +``` +stackit beta vpn connection create [flags] +``` + +### Examples + +``` + Create a VPN connection + $ stackit beta vpn connection create --gateway-id xxx --display-name my-connection --tunnel1-remote-address 1.2.3.4 --tunnel2-remote-address 5.6.7.8 +``` + +### Options + +``` + --display-name string Required: A user friendly name for the connection. + --enabled Enable the connection (default true) + --gateway-id string Required: Gateway ID + -h, --help Help for "stackit beta vpn connection create" + --labels stringToString Map of custom labels. Key and values must be a string with max 63 chars, start/end with alphanumeric. The key of a label follows the same rules as the LabelValue except that it cannot be empty. (example: foo=bar) (default []) + --local-subnets strings Defaults to 0.0.0.0/0 for Route-based VPN configurations. Mandatory for Policy-based. + --remote-subnets strings Defaults to 0.0.0.0/0 for Route-based VPN configurations. Mandatory for Policy-based. + --static-routes strings Use this for route-based VPN. + --tunnel1-bgp-remote-asn int Required: Tunnel 1 BGP Remote ASN. + ASN for private use (reserved by IANA), both 16Bit and 32Bit ranges are valid (RFC 6996). + --tunnel1-peering-local-address string Tunnel 1 Peering Local Address. + The peering object defines the point-to-point IP configuration for the Tunnel Interface. These addresses serve as next-hop identifiers and are used for BGP peering sessions and can be used in Static Route-Based connectivity. + --tunnel1-peering-remote-address string Tunnel 1 Peering Remote Address + --tunnel1-phase1-dh-groups strings Tunnel 1 Phase 1 DH Groups. + The Diffie-Hellman Group. Required, except if AEAD algorithms are selected. (multiple of: [modp1024, modp2048, ecp256, ecp384, modp2048s256]) (default []) + --tunnel1-phase1-encryption-algorithms strings Required: Tunnel 1 Phase 1 Encryption Algorithms (multiple of: [aes256, aes128gcm16, aes256gcm16]) (default []) + --tunnel1-phase1-integrity-algorithms strings Required: Tunnel 1 Phase 1 Integrity Algorithms (multiple of: [sha1, sha2_256, sha2_384]) (default []) + --tunnel1-phase1-rekey-time int Tunnel 1 Phase 1 Rekey Time. + Time to schedule a IKE re-keying (in seconds). + --tunnel1-phase2-dh-groups strings Tunnel 1 Phase 2 DH Groups (multiple of: [modp1024, modp2048, ecp256, ecp384, modp2048s256]) (default []) + --tunnel1-phase2-dpd-action string Tunnel 1 Phase 2 DPD Action. + Action to perform for this CHILD_SA on DPD timeout. "clear": Closes the CHILD_SA and does not take further action. "restart": immediately tries to re-negotiate the CILD_SA under a fresh IKE_SA. (one of: [clear, restart]) + --tunnel1-phase2-encryption-algorithms strings Required: Tunnel 1 Phase 2 Encryption Algorithms (multiple of: [aes256, aes128gcm16, aes256gcm16]) (default []) + --tunnel1-phase2-integrity-algorithms strings Required: Tunnel 1 Phase 2 Integrity Algorithms (multiple of: [sha1, sha2_256, sha2_384]) (default []) + --tunnel1-phase2-rekey-time int Tunnel 1 Phase 2 Rekey Time. + Time to schedule a Child SA re-keying (in seconds). + --tunnel1-phase2-start-action string Tunnel 1 Phase 2 Start Action. + Action to perform after loading the connection configuration. "none": The connection will be loaded but needs to be manually initiated. "start": initiates the connection actively. (one of: [none, start]) + --tunnel1-pre-shared-key string Required: Tunnel 1 Pre Shared Key. + A Pre-Shared Key for authentication. Required in create-requests, optional in update-requests and omitted in every response. + --tunnel1-remote-address string Tunnel 1 Remote Address + --tunnel2-bgp-remote-asn int Tunnel 2 BGP Remote ASN + --tunnel2-peering-local-address string Tunnel 2 Peering Local Address. + The peering object defines the point-to-point IP configuration for the Tunnel Interface. These addresses serve as next-hop identifiers and are used for BGP peering sessions and can be used in Static Route-Based connectivity. + --tunnel2-peering-remote-address string Tunnel 2 Peering Remote Address + --tunnel2-phase1-dh-groups strings Tunnel 2 Phase 1 DH Groups + The Diffie-Hellman Group. Required, except if AEAD algorithms are selected. (multiple of: [modp1024, modp2048, ecp256, ecp384, modp2048s256]) (default []) + --tunnel2-phase1-encryption-algorithms strings Required: Tunnel 2 Phase 1 Encryption Algorithms (multiple of: [aes256, aes128gcm16, aes256gcm16]) (default []) + --tunnel2-phase1-integrity-algorithms strings Required: Tunnel 2 Phase 1 Integrity Algorithms (multiple of: [sha1, sha2_256, sha2_384]) (default []) + --tunnel2-phase1-rekey-time int Tunnel 2 Phase 1 Rekey Time. + Time to schedule a IKE re-keying (in seconds). + --tunnel2-phase2-dh-groups strings Tunnel 2 Phase 2 DH Groups (multiple of: [modp1024, modp2048, ecp256, ecp384, modp2048s256]) (default []) + --tunnel2-phase2-dpd-action string Tunnel 2 Phase 2 DPD Action. + Action to perform for this CHILD_SA on DPD timeout. "clear": Closes the CHILD_SA and does not take further action. "restart": immediately tries to re-negotiate the CILD_SA under a fresh IKE_SA. (one of: [clear, restart]) + --tunnel2-phase2-encryption-algorithms strings Required: Tunnel 2 Phase 2 Encryption Algorithms (multiple of: [aes256, aes128gcm16, aes256gcm16]) (default []) + --tunnel2-phase2-integrity-algorithms strings Required: Tunnel 2 Phase 2 Integrity Algorithms (multiple of: [sha1, sha2_256, sha2_384]) (default []) + --tunnel2-phase2-rekey-time int Tunnel 2 Phase 2 Rekey Time. + Time to schedule a Child SA re-keying (in seconds). + --tunnel2-phase2-start-action string Tunnel 2 Phase 2 Start Action. + Default: "start" + Enum: "none" "start" + Action to perform after loading the connection configuration. "none": The connection will be loaded but needs to be manually initiated. "start": initiates the connection actively. (one of: [none, start]) + --tunnel2-pre-shared-key string Required: Tunnel 2 Pre Shared Key. + A Pre-Shared Key for authentication. Required in create-requests, optional in update-requests and omitted in every response. + --tunnel2-remote-address string Tunnel 2 Remote Address +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections + diff --git a/docs/stackit_beta_vpn_connection_delete.md b/docs/stackit_beta_vpn_connection_delete.md new file mode 100644 index 000000000..b66d0dc2f --- /dev/null +++ b/docs/stackit_beta_vpn_connection_delete.md @@ -0,0 +1,41 @@ +## stackit beta vpn connection delete + +Deletes a VPN connection + +### Synopsis + +Deletes a VPN connection. + +``` +stackit beta vpn connection delete CONNECTION_ID [flags] +``` + +### Examples + +``` + Delete a VPN connection + $ stackit beta vpn connection delete xxx --gateway-id yyy +``` + +### Options + +``` + --gateway-id string Gateway ID + -h, --help Help for "stackit beta vpn connection delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections + diff --git a/docs/stackit_beta_vpn_connection_describe.md b/docs/stackit_beta_vpn_connection_describe.md new file mode 100644 index 000000000..dc1ddd391 --- /dev/null +++ b/docs/stackit_beta_vpn_connection_describe.md @@ -0,0 +1,41 @@ +## stackit beta vpn connection describe + +Shows details of a VPN connection + +### Synopsis + +Shows details of a VPN connection. + +``` +stackit beta vpn connection describe CONNECTION_ID [flags] +``` + +### Examples + +``` + Show details of a VPN connection + $ stackit beta vpn connection describe xxx --gateway-id yyy +``` + +### Options + +``` + --gateway-id string Gateway ID + -h, --help Help for "stackit beta vpn connection describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections + diff --git a/docs/stackit_beta_vpn_connection_list.md b/docs/stackit_beta_vpn_connection_list.md new file mode 100644 index 000000000..bf1b935ba --- /dev/null +++ b/docs/stackit_beta_vpn_connection_list.md @@ -0,0 +1,41 @@ +## stackit beta vpn connection list + +Lists all VPN connections of a gateway + +### Synopsis + +Lists all VPN connections of a gateway. + +``` +stackit beta vpn connection list [flags] +``` + +### Examples + +``` + List all VPN connections of a gateway + $ stackit beta vpn connection list --gateway-id xxx +``` + +### Options + +``` + --gateway-id string Gateway ID + -h, --help Help for "stackit beta vpn connection list" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections + diff --git a/docs/stackit_beta_vpn_connection_status.md b/docs/stackit_beta_vpn_connection_status.md new file mode 100644 index 000000000..8137de7ef --- /dev/null +++ b/docs/stackit_beta_vpn_connection_status.md @@ -0,0 +1,41 @@ +## stackit beta vpn connection status + +Shows the status of a VPN connection + +### Synopsis + +Shows the status of a VPN connection. + +``` +stackit beta vpn connection status CONNECTION_ID [flags] +``` + +### Examples + +``` + Show status of a VPN connection + $ stackit beta vpn connection status xxx --gateway-id yyy +``` + +### Options + +``` + --gateway-id string Gateway ID + -h, --help Help for "stackit beta vpn connection status" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit beta vpn connection](./stackit_beta_vpn_connection.md) - Provides functionality for VPN connections + diff --git a/docs/stackit_config.md b/docs/stackit_config.md index 664b5dcd7..e97811c2b 100644 --- a/docs/stackit_config.md +++ b/docs/stackit_config.md @@ -26,10 +26,10 @@ stackit config [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_list.md b/docs/stackit_config_list.md index 6d08e1888..a3277fd02 100644 --- a/docs/stackit_config_list.md +++ b/docs/stackit_config_list.md @@ -37,10 +37,10 @@ stackit config list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile.md b/docs/stackit_config_profile.md index 6407d11db..03ad96466 100644 --- a/docs/stackit_config_profile.md +++ b/docs/stackit_config_profile.md @@ -24,10 +24,10 @@ stackit config profile [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_create.md b/docs/stackit_config_profile_create.md index 595c96fad..ac44cdfd3 100644 --- a/docs/stackit_config_profile_create.md +++ b/docs/stackit_config_profile_create.md @@ -9,6 +9,7 @@ The profile name can be provided via the STACKIT_CLI_PROFILE environment variabl The environment variable takes precedence over the argument. If you do not want to set the profile as active, use the --no-set flag. If you want to create the new profile with the initial default configurations, use the --empty flag. +If you want to create the new profile and ignore the error for an already existing profile, use the --ignore-existing flag. ``` stackit config profile create PROFILE [flags] @@ -27,9 +28,10 @@ stackit config profile create PROFILE [flags] ### Options ``` - --empty Create the profile with the initial default configurations - -h, --help Help for "stackit config profile create" - --no-set Do not set the profile as the active profile + --empty Create the profile with the initial default configurations + -h, --help Help for "stackit config profile create" + --ignore-existing Suppress the error if the profile exists already. An existing profile will not be modified or overwritten + --no-set Do not set the profile as the active profile ``` ### Options inherited from parent commands @@ -37,10 +39,10 @@ stackit config profile create PROFILE [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_delete.md b/docs/stackit_config_profile_delete.md index 770e3563e..1e71abba3 100644 --- a/docs/stackit_config_profile_delete.md +++ b/docs/stackit_config_profile_delete.md @@ -29,10 +29,10 @@ stackit config profile delete PROFILE [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_export.md b/docs/stackit_config_profile_export.md index 906a20eac..9dda31cfb 100644 --- a/docs/stackit_config_profile_export.md +++ b/docs/stackit_config_profile_export.md @@ -32,10 +32,10 @@ stackit config profile export PROFILE_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_import.md b/docs/stackit_config_profile_import.md index 86f253fc8..a4b474cbe 100644 --- a/docs/stackit_config_profile_import.md +++ b/docs/stackit_config_profile_import.md @@ -34,10 +34,10 @@ stackit config profile import [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_list.md b/docs/stackit_config_profile_list.md index ba39080d0..caee7de76 100644 --- a/docs/stackit_config_profile_list.md +++ b/docs/stackit_config_profile_list.md @@ -31,10 +31,10 @@ stackit config profile list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_set.md b/docs/stackit_config_profile_set.md index 64f6126b2..249e6149a 100644 --- a/docs/stackit_config_profile_set.md +++ b/docs/stackit_config_profile_set.md @@ -31,10 +31,10 @@ stackit config profile set PROFILE [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_profile_unset.md b/docs/stackit_config_profile_unset.md index 5895391b0..31035e56d 100644 --- a/docs/stackit_config_profile_unset.md +++ b/docs/stackit_config_profile_unset.md @@ -29,10 +29,10 @@ stackit config profile unset [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index b1abf5662..2a49e8ea6 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -31,13 +31,18 @@ stackit config set [flags] ``` --allowed-url-domain string Domain name, used for the verification of the URLs that are given in the custom identity provider endpoint and "STACKIT curl" command --authorization-custom-endpoint string Authorization API base URL, used in calls to this API + --cdn-custom-endpoint string CDN API base URL, used in calls to this API --dns-custom-endpoint string DNS API base URL, used in calls to this API + --edge-custom-endpoint string Edge API base URL, used in calls to this API -h, --help Help for "stackit config set" --iaas-custom-endpoint string IaaS API base URL, used in calls to this API --identity-provider-custom-client-id string Identity Provider client ID, used for user authentication --identity-provider-custom-well-known-configuration string Identity Provider well-known OpenID configuration URL, used for user authentication + --intake-custom-endpoint string Intake API base URL, used in calls to this API + --kms-custom-endpoint string KMS API base URL, used in calls to this API --load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API --logme-custom-endpoint string LogMe API base URL, used in calls to this API + --logs-custom-endpoint string Logs API base URL, used in calls to this API --mariadb-custom-endpoint string MariaDB API base URL, used in calls to this API --mongodbflex-custom-endpoint string MongoDB Flex API base URL, used in calls to this API --object-storage-custom-endpoint string Object Storage API base URL, used in calls to this API @@ -53,7 +58,8 @@ stackit config set [flags] --serverbackup-custom-endpoint string Server Backup API base URL, used in calls to this API --service-account-custom-endpoint string Service Account API base URL, used in calls to this API --service-enablement-custom-endpoint string Service Enablement API base URL, used in calls to this API - --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect) + --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s + --sfs-custom-endpoint string SFS API base URL, used in calls to this API --ske-custom-endpoint string SKE API base URL, used in calls to this API --sqlserverflex-custom-endpoint string SQLServer Flex API base URL, used in calls to this API --token-custom-endpoint string Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication. @@ -64,10 +70,10 @@ stackit config set [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 4a48b759e..d76f3068c 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -27,15 +27,21 @@ stackit config unset [flags] ``` --allowed-url-domain Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands. If unset, defaults to stackit.cloud + --assume-yes If set, skips all confirmation prompts --async Configuration option to run commands asynchronously --authorization-custom-endpoint Authorization API base URL. If unset, uses the default base URL + --cdn-custom-endpoint Custom CDN endpoint URL. If unset, uses the default base URL --dns-custom-endpoint DNS API base URL. If unset, uses the default base URL + --edge-custom-endpoint Edge API base URL. If unset, uses the default base URL -h, --help Help for "stackit config unset" --iaas-custom-endpoint IaaS API base URL. If unset, uses the default base URL --identity-provider-custom-client-id Identity Provider client ID, used for user authentication --identity-provider-custom-well-known-configuration Identity Provider well-known OpenID configuration URL. If unset, uses the default identity provider + --intake-custom-endpoint Intake API base URL. If unset, uses the default base URL + --kms-custom-endpoint KMS API base URL. If unset, uses the default base URL --load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL --logme-custom-endpoint LogMe API base URL. If unset, uses the default base URL + --logs-custom-endpoint Logs API base URL. If unset, uses the default base URL --mariadb-custom-endpoint MariaDB API base URL. If unset, uses the default base URL --mongodbflex-custom-endpoint MongoDB Flex API base URL. If unset, uses the default base URL --object-storage-custom-endpoint Object Storage API base URL. If unset, uses the default base URL @@ -54,19 +60,14 @@ stackit config unset [flags] --serverbackup-custom-endpoint Server Backup base URL. If unset, uses the default base URL --service-account-custom-endpoint Service Account API base URL. If unset, uses the default base URL --service-enablement-custom-endpoint Service Enablement API base URL. If unset, uses the default base URL - --session-time-limit Maximum time before authentication is required again. If unset, defaults to 2h + --session-time-limit Maximum time before authentication is required again. If unset, defaults to 12h + --sfs-custom-endpoint SFS API base URL. If unset, uses the default base URL --ske-custom-endpoint SKE API base URL. If unset, uses the default base URL --sqlserverflex-custom-endpoint SQLServer Flex API base URL. If unset, uses the default base URL --token-custom-endpoint Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication. --verbosity Verbosity of the CLI ``` -### Options inherited from parent commands - -``` - -y, --assume-yes If set, skips all confirmation prompts -``` - ### SEE ALSO * [stackit config](./stackit_config.md) - Provides functionality for CLI configuration options diff --git a/docs/stackit_curl.md b/docs/stackit_curl.md index ef387edb5..573235b54 100644 --- a/docs/stackit_curl.md +++ b/docs/stackit_curl.md @@ -35,7 +35,7 @@ stackit curl URL [flags] -h, --help Help for "stackit curl" --include If set, response headers are added to the output --output string Writes output to provided file instead of printing to console - -X, --request string HTTP method, defaults to GET + -X, --request string HTTP method, defaults to GET (one of: [GET, HEAD, POST, PUT, PATCH, DELETE, CONNECT, OPTIONS, TRACE]) (default "GET") ``` ### Options inherited from parent commands @@ -43,10 +43,10 @@ stackit curl URL [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns.md b/docs/stackit_dns.md index a3e7279ba..7f26e9946 100644 --- a/docs/stackit_dns.md +++ b/docs/stackit_dns.md @@ -21,10 +21,10 @@ stackit dns [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_record-set.md b/docs/stackit_dns_record-set.md index 8796d62f2..434405a94 100644 --- a/docs/stackit_dns_record-set.md +++ b/docs/stackit_dns_record-set.md @@ -21,10 +21,10 @@ stackit dns record-set [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_record-set_create.md b/docs/stackit_dns_record-set_create.md index 4f51534ad..adb18a589 100644 --- a/docs/stackit_dns_record-set_create.md +++ b/docs/stackit_dns_record-set_create.md @@ -24,8 +24,8 @@ stackit dns record-set create [flags] -h, --help Help for "stackit dns record-set create" --name string Name of the record, should be compliant with RFC1035, Section 2.3.4 --record strings Records belonging to the record set - --ttl int Time to live, if not provided defaults to the zone's default TTL - --type string Record type, one of ["A" "AAAA" "SOA" "CNAME" "NS" "MX" "TXT" "SRV" "PTR" "ALIAS" "DNAME" "CAA" "DNSKEY" "DS" "LOC" "NAPTR" "SSHFP" "TLSA" "URI" "CERT" "SVCB" "TYPE" "CSYNC" "HINFO" "HTTPS"] (default "A") + --ttl int32 Time to live, if not provided defaults to the zone's default TTL + --type string Record type, (one of: [A, AAAA, SOA, CNAME, NS, MX, TXT, SRV, PTR, ALIAS, DNAME, CAA, DNSKEY, DS, LOC, NAPTR, SSHFP, TLSA, URI, CERT, SVCB, TYPE, CSYNC, HINFO, HTTPS]) (default "A") --zone-id string Zone ID ``` @@ -34,10 +34,10 @@ stackit dns record-set create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_record-set_delete.md b/docs/stackit_dns_record-set_delete.md index 487c5a621..a4c5710eb 100644 --- a/docs/stackit_dns_record-set_delete.md +++ b/docs/stackit_dns_record-set_delete.md @@ -29,10 +29,10 @@ stackit dns record-set delete RECORD_SET_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_record-set_describe.md b/docs/stackit_dns_record-set_describe.md index fb9ab8873..b22e68990 100644 --- a/docs/stackit_dns_record-set_describe.md +++ b/docs/stackit_dns_record-set_describe.md @@ -32,10 +32,10 @@ stackit dns record-set describe RECORD_SET_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_record-set_list.md b/docs/stackit_dns_record-set_list.md index 75cc555a2..a1d49fcdf 100644 --- a/docs/stackit_dns_record-set_list.md +++ b/docs/stackit_dns_record-set_list.md @@ -38,7 +38,7 @@ stackit dns record-set list [flags] --inactive Filter for inactive record sets. Deleted record sets are always inactive and will be included when this flag is set --limit int Maximum number of entries to list --name-like string Filter by name - --order-by-name string Order by name, one of ["asc" "desc"] + --order-by-name string Order by name, (one of: [asc, desc]) --page-size int Number of items fetched in each API call. Does not affect the number of items in the command output (default 100) --zone-id string Zone ID ``` @@ -48,10 +48,10 @@ stackit dns record-set list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_record-set_update.md b/docs/stackit_dns_record-set_update.md index 9d369c4f6..0e93ba01d 100644 --- a/docs/stackit_dns_record-set_update.md +++ b/docs/stackit_dns_record-set_update.md @@ -24,7 +24,7 @@ stackit dns record-set update RECORD_SET_ID [flags] -h, --help Help for "stackit dns record-set update" --name string Name of the record, should be compliant with RFC1035, Section 2.3.4 --record strings Records belonging to the record set. If this flag is used, records already created that aren't set when running the command will be deleted - --ttl int Time to live, if not provided defaults to the zone's default TTL + --ttl int32 Time to live, if not provided defaults to the zone's default TTL --zone-id string Zone ID ``` @@ -33,10 +33,10 @@ stackit dns record-set update RECORD_SET_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone.md b/docs/stackit_dns_zone.md index a5e705ded..4693699e7 100644 --- a/docs/stackit_dns_zone.md +++ b/docs/stackit_dns_zone.md @@ -21,10 +21,10 @@ stackit dns zone [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone_clone.md b/docs/stackit_dns_zone_clone.md index 2b944f077..3dacbe2a2 100644 --- a/docs/stackit_dns_zone_clone.md +++ b/docs/stackit_dns_zone_clone.md @@ -38,10 +38,10 @@ stackit dns zone clone ZONE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone_create.md b/docs/stackit_dns_zone_create.md index 0a0efde1c..0f089de3c 100644 --- a/docs/stackit_dns_zone_create.md +++ b/docs/stackit_dns_zone_create.md @@ -25,18 +25,18 @@ stackit dns zone create [flags] ``` --acl string Access control list --contact-email string Contact email for the zone - --default-ttl int Default time to live (default 1000) + --default-ttl int32 Default time to live (default 1000) --description string Description of the zone --dns-name string Fully qualified domain name of the DNS zone - --expire-time int Expire time + --expire-time int32 Expire time -h, --help Help for "stackit dns zone create" --is-reverse-zone Is reverse zone --name string User given name of the zone - --negative-cache int Negative cache + --negative-cache int32 Negative cache --primary strings Primary name server for secondary zone - --refresh-time int Refresh time - --retry-time int Retry time - --type string Zone type, one of: ["primary" "secondary"] + --refresh-time int32 Refresh time + --retry-time int32 Retry time + --type string Zone type, (one of: [primary, secondary, ]) ``` ### Options inherited from parent commands @@ -44,10 +44,10 @@ stackit dns zone create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone_delete.md b/docs/stackit_dns_zone_delete.md index 466d51f72..c401b7b45 100644 --- a/docs/stackit_dns_zone_delete.md +++ b/docs/stackit_dns_zone_delete.md @@ -28,10 +28,10 @@ stackit dns zone delete ZONE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone_describe.md b/docs/stackit_dns_zone_describe.md index 896a3ef9d..f9815abc3 100644 --- a/docs/stackit_dns_zone_describe.md +++ b/docs/stackit_dns_zone_describe.md @@ -31,10 +31,10 @@ stackit dns zone describe ZONE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone_list.md b/docs/stackit_dns_zone_list.md index bb9e01fd1..c108de65d 100644 --- a/docs/stackit_dns_zone_list.md +++ b/docs/stackit_dns_zone_list.md @@ -35,7 +35,7 @@ stackit dns zone list [flags] --include-deleted Includes successfully deleted zones (if unset, these are filtered out) --limit int Maximum number of entries to list --name-like string Filter by name - --order-by-name string Order by name, one of ["asc" "desc"] + --order-by-name string Order by name, (one of: [asc, desc]) --page-size int Number of items fetched in each API call. Does not affect the number of items in the command output (default 100) ``` @@ -44,10 +44,10 @@ stackit dns zone list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_dns_zone_update.md b/docs/stackit_dns_zone_update.md index 240885c1b..7e0e00bcf 100644 --- a/docs/stackit_dns_zone_update.md +++ b/docs/stackit_dns_zone_update.md @@ -22,15 +22,15 @@ stackit dns zone update ZONE_ID [flags] ``` --acl string Access control list --contact-email string Contact email for the zone - --default-ttl int Default time to live (default 1000) + --default-ttl int32 Default time to live (default 1000) --description string Description of the zone - --expire-time int Expire time + --expire-time int32 Expire time -h, --help Help for "stackit dns zone update" --name string User given name of the zone - --negative-cache int Negative cache + --negative-cache int32 Negative cache --primary strings Primary name server for secondary zone - --refresh-time int Refresh time - --retry-time int Retry time + --refresh-time int32 Refresh time + --retry-time int32 Retry time ``` ### Options inherited from parent commands @@ -38,10 +38,10 @@ stackit dns zone update ZONE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git.md b/docs/stackit_git.md index 2a9b072e2..333e61dca 100644 --- a/docs/stackit_git.md +++ b/docs/stackit_git.md @@ -21,10 +21,10 @@ stackit git [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git_flavor.md b/docs/stackit_git_flavor.md index c2ec85a08..c89bc0254 100644 --- a/docs/stackit_git_flavor.md +++ b/docs/stackit_git_flavor.md @@ -21,10 +21,10 @@ stackit git flavor [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git_flavor_list.md b/docs/stackit_git_flavor_list.md index a8fc54b0f..194582097 100644 --- a/docs/stackit_git_flavor_list.md +++ b/docs/stackit_git_flavor_list.md @@ -32,10 +32,10 @@ stackit git flavor list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git_instance.md b/docs/stackit_git_instance.md index 5f7c6d243..077695d87 100644 --- a/docs/stackit_git_instance.md +++ b/docs/stackit_git_instance.md @@ -21,10 +21,10 @@ stackit git instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO @@ -33,5 +33,5 @@ stackit git instance [flags] * [stackit git instance create](./stackit_git_instance_create.md) - Creates STACKIT Git instance * [stackit git instance delete](./stackit_git_instance_delete.md) - Deletes STACKIT Git instance * [stackit git instance describe](./stackit_git_instance_describe.md) - Describes STACKIT Git instance -* [stackit git instance list](./stackit_git_instance_list.md) - Lists all instances of STACKIT Git. +* [stackit git instance list](./stackit_git_instance_list.md) - Lists all instances of STACKIT Git diff --git a/docs/stackit_git_instance_create.md b/docs/stackit_git_instance_create.md index e5d8bba93..c5d42ec3c 100644 --- a/docs/stackit_git_instance_create.md +++ b/docs/stackit_git_instance_create.md @@ -17,10 +17,10 @@ stackit git instance create [flags] $ stackit git instance create --name my-new-instance Create a instance with name 'my-new-instance' and flavor - $ stackit git instance create --name my-new-instance --flavor git-100' + $ stackit git instance create --name my-new-instance --flavor git-100 Create a instance with name 'my-new-instance' and acl - $ stackit git instance create --name my-new-instance --acl 1.1.1.1/1' + $ stackit git instance create --name my-new-instance --acl 1.1.1.1/1 ``` ### Options @@ -37,10 +37,10 @@ stackit git instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git_instance_delete.md b/docs/stackit_git_instance_delete.md index df0a6d46a..681e379fd 100644 --- a/docs/stackit_git_instance_delete.md +++ b/docs/stackit_git_instance_delete.md @@ -28,10 +28,10 @@ stackit git instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git_instance_describe.md b/docs/stackit_git_instance_describe.md index 90716803e..c355957ea 100644 --- a/docs/stackit_git_instance_describe.md +++ b/docs/stackit_git_instance_describe.md @@ -28,10 +28,10 @@ stackit git instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_git_instance_list.md b/docs/stackit_git_instance_list.md index 96afe06d4..06d33564a 100644 --- a/docs/stackit_git_instance_list.md +++ b/docs/stackit_git_instance_list.md @@ -1,6 +1,6 @@ ## stackit git instance list -Lists all instances of STACKIT Git. +Lists all instances of STACKIT Git ### Synopsis @@ -32,10 +32,10 @@ stackit git instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_image.md b/docs/stackit_image.md index 4120c26e6..eec4b558b 100644 --- a/docs/stackit_image.md +++ b/docs/stackit_image.md @@ -21,10 +21,10 @@ stackit image [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_image_create.md b/docs/stackit_image_create.md index eb8a0a3e9..172eea4a3 100644 --- a/docs/stackit_image_create.md +++ b/docs/stackit_image_create.md @@ -56,10 +56,10 @@ stackit image create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_image_delete.md b/docs/stackit_image_delete.md index bbe36d37d..b8e38c272 100644 --- a/docs/stackit_image_delete.md +++ b/docs/stackit_image_delete.md @@ -28,10 +28,10 @@ stackit image delete IMAGE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_image_describe.md b/docs/stackit_image_describe.md index 35403a150..513448e72 100644 --- a/docs/stackit_image_describe.md +++ b/docs/stackit_image_describe.md @@ -28,10 +28,10 @@ stackit image describe IMAGE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_image_list.md b/docs/stackit_image_list.md index eae2a3409..24c2a8e42 100644 --- a/docs/stackit_image_list.md +++ b/docs/stackit_image_list.md @@ -13,7 +13,7 @@ stackit image list [flags] ### Examples ``` - List all images + List images in your project $ stackit image list List images with label @@ -21,11 +21,15 @@ stackit image list [flags] List the first 10 images $ stackit image list --limit=10 + + List all images + $ stackit image list --all ``` ### Options ``` + --all List all images available -h, --help Help for "stackit image list" --label-selector string Filter by label --limit int Limit the output to the first n elements @@ -36,10 +40,10 @@ stackit image list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_image_update.md b/docs/stackit_image_update.md index d088d9962..c2064e263 100644 --- a/docs/stackit_image_update.md +++ b/docs/stackit_image_update.md @@ -51,10 +51,10 @@ stackit image update IMAGE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_key-pair.md b/docs/stackit_key-pair.md index 6e3aff7fb..3049d79fc 100644 --- a/docs/stackit_key-pair.md +++ b/docs/stackit_key-pair.md @@ -21,10 +21,10 @@ stackit key-pair [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_key-pair_create.md b/docs/stackit_key-pair_create.md index 04fbb9561..d33b5f993 100644 --- a/docs/stackit_key-pair_create.md +++ b/docs/stackit_key-pair_create.md @@ -40,10 +40,10 @@ stackit key-pair create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_key-pair_delete.md b/docs/stackit_key-pair_delete.md index b9dc10cda..9c22abd45 100644 --- a/docs/stackit_key-pair_delete.md +++ b/docs/stackit_key-pair_delete.md @@ -28,10 +28,10 @@ stackit key-pair delete KEY_PAIR_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_key-pair_describe.md b/docs/stackit_key-pair_describe.md index fa68ec225..90b13df32 100644 --- a/docs/stackit_key-pair_describe.md +++ b/docs/stackit_key-pair_describe.md @@ -32,10 +32,10 @@ stackit key-pair describe KEY_PAIR_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_key-pair_list.md b/docs/stackit_key-pair_list.md index f4af20042..a8c1c162f 100644 --- a/docs/stackit_key-pair_list.md +++ b/docs/stackit_key-pair_list.md @@ -39,10 +39,10 @@ stackit key-pair list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_key-pair_update.md b/docs/stackit_key-pair_update.md index 6c7399dd0..d2f31fb9e 100644 --- a/docs/stackit_key-pair_update.md +++ b/docs/stackit_key-pair_update.md @@ -29,10 +29,10 @@ stackit key-pair update KEY_PAIR_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_kms.md b/docs/stackit_kms.md new file mode 100644 index 000000000..24f9ac3ad --- /dev/null +++ b/docs/stackit_kms.md @@ -0,0 +1,37 @@ +## stackit kms + +Provides functionality for KMS + +### Synopsis + +Provides functionality for KMS. + +``` +stackit kms [flags] +``` + +### Options + +``` + -h, --help Help for "stackit kms" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys +* [stackit kms keyring](./stackit_kms_keyring.md) - Manage KMS key rings +* [stackit kms version](./stackit_kms_version.md) - Manage KMS key versions +* [stackit kms wrapping-key](./stackit_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/docs/stackit_kms_key.md b/docs/stackit_kms_key.md new file mode 100644 index 000000000..2a1c0ff65 --- /dev/null +++ b/docs/stackit_kms_key.md @@ -0,0 +1,40 @@ +## stackit kms key + +Manage KMS keys + +### Synopsis + +Provides functionality for key operations inside the KMS + +``` +stackit kms key [flags] +``` + +### Options + +``` + -h, --help Help for "stackit kms key" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms](./stackit_kms.md) - Provides functionality for KMS +* [stackit kms key create](./stackit_kms_key_create.md) - Creates a KMS key +* [stackit kms key delete](./stackit_kms_key_delete.md) - Deletes a KMS key +* [stackit kms key describe](./stackit_kms_key_describe.md) - Describe a KMS key +* [stackit kms key import](./stackit_kms_key_import.md) - Import a KMS key +* [stackit kms key list](./stackit_kms_key_list.md) - List all KMS keys +* [stackit kms key restore](./stackit_kms_key_restore.md) - Restore a key +* [stackit kms key rotate](./stackit_kms_key_rotate.md) - Rotate a key + diff --git a/docs/stackit_kms_key_create.md b/docs/stackit_kms_key_create.md new file mode 100644 index 000000000..3b6bfc18f --- /dev/null +++ b/docs/stackit_kms_key_create.md @@ -0,0 +1,62 @@ +## stackit kms key create + +Creates a KMS key + +### Synopsis + +Creates a KMS key. + +``` +stackit kms key create [flags] +``` + +### Examples + +``` + Create a symmetric AES key (AES-256) with the name "symm-aes-gcm" under the key ring "my-keyring-id" + $ stackit kms key create --keyring-id "my-keyring-id" --algorithm "aes_256_gcm" --name "symm-aes-gcm" --purpose "symmetric_encrypt_decrypt" --protection "software" + + Create an asymmetric RSA encryption key (RSA-2048) + $ stackit kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "prod-orders-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" + + Create a message authentication key (HMAC-SHA512) + $ stackit kms key create --keyring-id "my-keyring-id" --algorithm "hmac_sha512" --name "api-mac-key" --purpose "message_authentication_code" --protection "software" + + Create an ECDSA P-256 key for signing & verification + $ stackit kms key create --keyring-id "my-keyring-id" --algorithm "ecdsa_p256_sha256" --name "signing-ecdsa-p256" --purpose "asymmetric_sign_verify" --protection "software" + + Create an import-only key (versions must be imported) + $ stackit kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "ext-managed-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --import-only + + Create a key and print the result as YAML + $ stackit kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "yaml-output-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --output yaml +``` + +### Options + +``` + --algorithm string En-/Decryption / signing algorithm. (one of: [aes_256_gcm, rsa_2048_oaep_sha256, rsa_3072_oaep_sha256, rsa_4096_oaep_sha256, rsa_4096_oaep_sha512, hmac_sha256, hmac_sha384, hmac_sha512, ecdsa_p256_sha256, ecdsa_p384_sha384, ecdsa_p521_sha512]) + --description string Optional description of the key + -h, --help Help for "stackit kms key create" + --import-only States whether versions can be created or only imported + --keyring-id string ID of the KMS key ring + --name string The display name to distinguish multiple keys + --protection string The underlying system that is responsible for protecting the key material. (one of: [software]) + --purpose string Purpose of the key. (one of: [symmetric_encrypt_decrypt, asymmetric_encrypt_decrypt, message_authentication_code, asymmetric_sign_verify]) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_key_delete.md b/docs/stackit_kms_key_delete.md new file mode 100644 index 000000000..2871be570 --- /dev/null +++ b/docs/stackit_kms_key_delete.md @@ -0,0 +1,41 @@ +## stackit kms key delete + +Deletes a KMS key + +### Synopsis + +Deletes a KMS key inside a specific key ring. + +``` +stackit kms key delete KEY_ID [flags] +``` + +### Examples + +``` + Delete a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id" + $ stackit kms key delete "MY_KEY_ID" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms key delete" + --keyring-id string ID of the KMS key ring where the key is stored +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_key_describe.md b/docs/stackit_kms_key_describe.md new file mode 100644 index 000000000..1d87c1bee --- /dev/null +++ b/docs/stackit_kms_key_describe.md @@ -0,0 +1,41 @@ +## stackit kms key describe + +Describe a KMS key + +### Synopsis + +Describe a KMS key + +``` +stackit kms key describe KEY_ID [flags] +``` + +### Examples + +``` + Describe a KMS key with ID xxx of keyring yyy + $ stackit kms key describe xxx --keyring-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit kms key describe" + --keyring-id string Key Ring ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_key_import.md b/docs/stackit_kms_key_import.md new file mode 100644 index 000000000..adc649962 --- /dev/null +++ b/docs/stackit_kms_key_import.md @@ -0,0 +1,46 @@ +## stackit kms key import + +Import a KMS key + +### Synopsis + +After encrypting the secret with the wrapping key’s public key and Base64-encoding it, import it as a new version of the specified KMS key. + +``` +stackit kms key import KEY_ID [flags] +``` + +### Examples + +``` + Import a new version for the given KMS key "MY_KEY_ID" from literal value + $ stackit kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "BASE64_VALUE" --wrapping-key-id "MY_WRAPPING_KEY_ID" + + Import from a file + $ stackit kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "@path/to/wrapped.key.b64" --wrapping-key-id "MY_WRAPPING_KEY_ID" +``` + +### Options + +``` + -h, --help Help for "stackit kms key import" + --keyring-id string ID of the KMS key ring + --wrapped-key string The wrapped key material to be imported. Base64-encoded. Pass the value directly or a file path (e.g. @path/to/wrapped.key.b64) + --wrapping-key-id string The unique id of the wrapping key the key material has been wrapped with +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_key_list.md b/docs/stackit_kms_key_list.md new file mode 100644 index 000000000..af99df0ba --- /dev/null +++ b/docs/stackit_kms_key_list.md @@ -0,0 +1,44 @@ +## stackit kms key list + +List all KMS keys + +### Synopsis + +List all KMS keys inside a key ring. + +``` +stackit kms key list [flags] +``` + +### Examples + +``` + List all KMS keys for the key ring "my-keyring-id" + $ stackit kms key list --keyring-id "my-keyring-id" + + List all KMS keys in JSON format + $ stackit kms key list --keyring-id "my-keyring-id" --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit kms key list" + --keyring-id string ID of the KMS key ring where the key is stored +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_key_restore.md b/docs/stackit_kms_key_restore.md new file mode 100644 index 000000000..18a20eb69 --- /dev/null +++ b/docs/stackit_kms_key_restore.md @@ -0,0 +1,41 @@ +## stackit kms key restore + +Restore a key + +### Synopsis + +Restores the given key from deletion. + +``` +stackit kms key restore KEY_ID [flags] +``` + +### Examples + +``` + Restore a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id" that was scheduled for deletion. + $ stackit kms key restore "MY_KEY_ID" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms key restore" + --keyring-id string ID of the KMS key ring where the key is stored +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_key_rotate.md b/docs/stackit_kms_key_rotate.md new file mode 100644 index 000000000..1ef49d35d --- /dev/null +++ b/docs/stackit_kms_key_rotate.md @@ -0,0 +1,41 @@ +## stackit kms key rotate + +Rotate a key + +### Synopsis + +Rotates the given key. + +``` +stackit kms key rotate KEY_ID [flags] +``` + +### Examples + +``` + Rotate a KMS key "MY_KEY_ID" and increase its version inside the key ring "my-keyring-id". + $ stackit kms key rotate "MY_KEY_ID" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms key rotate" + --keyring-id string ID of the KMS key ring where the key is stored +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms key](./stackit_kms_key.md) - Manage KMS keys + diff --git a/docs/stackit_kms_keyring.md b/docs/stackit_kms_keyring.md new file mode 100644 index 000000000..979eb7ffe --- /dev/null +++ b/docs/stackit_kms_keyring.md @@ -0,0 +1,37 @@ +## stackit kms keyring + +Manage KMS key rings + +### Synopsis + +Provides functionality for key ring operations inside the KMS + +``` +stackit kms keyring [flags] +``` + +### Options + +``` + -h, --help Help for "stackit kms keyring" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms](./stackit_kms.md) - Provides functionality for KMS +* [stackit kms keyring create](./stackit_kms_keyring_create.md) - Creates a KMS key ring +* [stackit kms keyring delete](./stackit_kms_keyring_delete.md) - Deletes a KMS key ring +* [stackit kms keyring describe](./stackit_kms_keyring_describe.md) - Describe a KMS key ring +* [stackit kms keyring list](./stackit_kms_keyring_list.md) - Lists all KMS key rings + diff --git a/docs/stackit_kms_keyring_create.md b/docs/stackit_kms_keyring_create.md new file mode 100644 index 000000000..fdc6ff42a --- /dev/null +++ b/docs/stackit_kms_keyring_create.md @@ -0,0 +1,48 @@ +## stackit kms keyring create + +Creates a KMS key ring + +### Synopsis + +Creates a KMS key ring. + +``` +stackit kms keyring create [flags] +``` + +### Examples + +``` + Create a KMS key ring with name "my-keyring" + $ stackit kms keyring create --name my-keyring + + Create a KMS key ring with a description + $ stackit kms keyring create --name my-keyring --description my-description + + Create a KMS key ring and print the result as YAML + $ stackit kms keyring create --name my-keyring -o yaml +``` + +### Options + +``` + --description string Optional description of the key ring + -h, --help Help for "stackit kms keyring create" + --name string Name of the KMS key ring +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms keyring](./stackit_kms_keyring.md) - Manage KMS key rings + diff --git a/docs/stackit_kms_keyring_delete.md b/docs/stackit_kms_keyring_delete.md new file mode 100644 index 000000000..bf43c1968 --- /dev/null +++ b/docs/stackit_kms_keyring_delete.md @@ -0,0 +1,40 @@ +## stackit kms keyring delete + +Deletes a KMS key ring + +### Synopsis + +Deletes a KMS key ring. + +``` +stackit kms keyring delete KEYRING-ID [flags] +``` + +### Examples + +``` + Delete a KMS key ring with ID "MY_KEYRING_ID" + $ stackit kms keyring delete "MY_KEYRING_ID" +``` + +### Options + +``` + -h, --help Help for "stackit kms keyring delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms keyring](./stackit_kms_keyring.md) - Manage KMS key rings + diff --git a/docs/stackit_kms_keyring_describe.md b/docs/stackit_kms_keyring_describe.md new file mode 100644 index 000000000..0ed1f688d --- /dev/null +++ b/docs/stackit_kms_keyring_describe.md @@ -0,0 +1,40 @@ +## stackit kms keyring describe + +Describe a KMS key ring + +### Synopsis + +Describe a KMS key ring + +``` +stackit kms keyring describe KEYRING_ID [flags] +``` + +### Examples + +``` + Describe a KMS key ring with ID xxx + $ stackit kms keyring describe xxx +``` + +### Options + +``` + -h, --help Help for "stackit kms keyring describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms keyring](./stackit_kms_keyring.md) - Manage KMS key rings + diff --git a/docs/stackit_kms_keyring_list.md b/docs/stackit_kms_keyring_list.md new file mode 100644 index 000000000..9b8398439 --- /dev/null +++ b/docs/stackit_kms_keyring_list.md @@ -0,0 +1,43 @@ +## stackit kms keyring list + +Lists all KMS key rings + +### Synopsis + +Lists all KMS key rings. + +``` +stackit kms keyring list [flags] +``` + +### Examples + +``` + List all KMS key rings + $ stackit kms keyring list + + List all KMS key rings in JSON format + $ stackit kms keyring list --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit kms keyring list" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms keyring](./stackit_kms_keyring.md) - Manage KMS key rings + diff --git a/docs/stackit_kms_version.md b/docs/stackit_kms_version.md new file mode 100644 index 000000000..d14706eb9 --- /dev/null +++ b/docs/stackit_kms_version.md @@ -0,0 +1,38 @@ +## stackit kms version + +Manage KMS key versions + +### Synopsis + +Provides functionality for key version operations inside the KMS + +``` +stackit kms version [flags] +``` + +### Options + +``` + -h, --help Help for "stackit kms version" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms](./stackit_kms.md) - Provides functionality for KMS +* [stackit kms version destroy](./stackit_kms_version_destroy.md) - Destroy a key version +* [stackit kms version disable](./stackit_kms_version_disable.md) - Disable a key version +* [stackit kms version enable](./stackit_kms_version_enable.md) - Enable a key version +* [stackit kms version list](./stackit_kms_version_list.md) - List all key versions +* [stackit kms version restore](./stackit_kms_version_restore.md) - Restore a key version + diff --git a/docs/stackit_kms_version_destroy.md b/docs/stackit_kms_version_destroy.md new file mode 100644 index 000000000..b9742f909 --- /dev/null +++ b/docs/stackit_kms_version_destroy.md @@ -0,0 +1,42 @@ +## stackit kms version destroy + +Destroy a key version + +### Synopsis + +Removes the key material of a version. + +``` +stackit kms version destroy VERSION_NUMBER [flags] +``` + +### Examples + +``` + Destroy key version "42" for the key "my-key-id" inside the key ring "my-keyring-id" + $ stackit kms version destroy 42 --key-id "my-key-id" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms version destroy" + --key-id string ID of the key + --keyring-id string ID of the KMS key ring +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms version](./stackit_kms_version.md) - Manage KMS key versions + diff --git a/docs/stackit_kms_version_disable.md b/docs/stackit_kms_version_disable.md new file mode 100644 index 000000000..bb0f1e911 --- /dev/null +++ b/docs/stackit_kms_version_disable.md @@ -0,0 +1,42 @@ +## stackit kms version disable + +Disable a key version + +### Synopsis + +Disable the given key version. + +``` +stackit kms version disable VERSION_NUMBER [flags] +``` + +### Examples + +``` + Disable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id" + $ stackit kms version disable 42 --key-id "my-key-id" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms version disable" + --key-id string ID of the key + --keyring-id string ID of the KMS key ring +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms version](./stackit_kms_version.md) - Manage KMS key versions + diff --git a/docs/stackit_kms_version_enable.md b/docs/stackit_kms_version_enable.md new file mode 100644 index 000000000..b080abbff --- /dev/null +++ b/docs/stackit_kms_version_enable.md @@ -0,0 +1,42 @@ +## stackit kms version enable + +Enable a key version + +### Synopsis + +Enable the given key version. + +``` +stackit kms version enable VERSION_NUMBER [flags] +``` + +### Examples + +``` + Enable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id" + $ stackit kms version enable 42 --key-id "my-key-id" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms version enable" + --key-id string ID of the key + --keyring-id string ID of the KMS key ring +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms version](./stackit_kms_version.md) - Manage KMS key versions + diff --git a/docs/stackit_kms_version_list.md b/docs/stackit_kms_version_list.md new file mode 100644 index 000000000..c29e1af42 --- /dev/null +++ b/docs/stackit_kms_version_list.md @@ -0,0 +1,45 @@ +## stackit kms version list + +List all key versions + +### Synopsis + +List all versions of a given key. + +``` +stackit kms version list [flags] +``` + +### Examples + +``` + List all key versions for the key "my-key-id" inside the key ring "my-keyring-id" + $ stackit kms version list --key-id "my-key-id" --keyring-id "my-keyring-id" + + List all key versions in JSON format + $ stackit kms version list --key-id "my-key-id" --keyring-id "my-keyring-id" -o json +``` + +### Options + +``` + -h, --help Help for "stackit kms version list" + --key-id string ID of the key + --keyring-id string ID of the KMS key ring +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms version](./stackit_kms_version.md) - Manage KMS key versions + diff --git a/docs/stackit_kms_version_restore.md b/docs/stackit_kms_version_restore.md new file mode 100644 index 000000000..0f016b960 --- /dev/null +++ b/docs/stackit_kms_version_restore.md @@ -0,0 +1,42 @@ +## stackit kms version restore + +Restore a key version + +### Synopsis + +Restores the specified version of a key. + +``` +stackit kms version restore VERSION_NUMBER [flags] +``` + +### Examples + +``` + Restore key version "42" for the key "my-key-id" inside the key ring "my-keyring-id" + $ stackit kms version restore 42 --key-id "my-key-id" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms version restore" + --key-id string ID of the key + --keyring-id string ID of the KMS key ring +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms version](./stackit_kms_version.md) - Manage KMS key versions + diff --git a/docs/stackit_kms_wrapping-key.md b/docs/stackit_kms_wrapping-key.md new file mode 100644 index 000000000..e95accf98 --- /dev/null +++ b/docs/stackit_kms_wrapping-key.md @@ -0,0 +1,37 @@ +## stackit kms wrapping-key + +Manage KMS wrapping keys + +### Synopsis + +Provides functionality for wrapping key operations inside the KMS + +``` +stackit kms wrapping-key [flags] +``` + +### Options + +``` + -h, --help Help for "stackit kms wrapping-key" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms](./stackit_kms.md) - Provides functionality for KMS +* [stackit kms wrapping-key create](./stackit_kms_wrapping-key_create.md) - Creates a KMS wrapping key +* [stackit kms wrapping-key delete](./stackit_kms_wrapping-key_delete.md) - Deletes a KMS wrapping key +* [stackit kms wrapping-key describe](./stackit_kms_wrapping-key_describe.md) - Describe a KMS wrapping key +* [stackit kms wrapping-key list](./stackit_kms_wrapping-key_list.md) - Lists all KMS wrapping keys + diff --git a/docs/stackit_kms_wrapping-key_create.md b/docs/stackit_kms_wrapping-key_create.md new file mode 100644 index 000000000..eb74a0374 --- /dev/null +++ b/docs/stackit_kms_wrapping-key_create.md @@ -0,0 +1,49 @@ +## stackit kms wrapping-key create + +Creates a KMS wrapping key + +### Synopsis + +Creates a KMS wrapping key. + +``` +stackit kms wrapping-key create [flags] +``` + +### Examples + +``` + Create a symmetric (RSA + AES) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id" + $ stackit kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256_aes_256_key_wrap" --name "my-wrapping-key-name" --purpose "wrap_symmetric_key" --protection "software" + + Create an asymmetric (RSA) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id" + $ stackit kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_3072_oaep_sha256" --name "my-wrapping-key-name" --purpose "wrap_asymmetric_key" --protection "software" +``` + +### Options + +``` + --algorithm string En-/Decryption / signing algorithm. (one of: [rsa_2048_oaep_sha256, rsa_3072_oaep_sha256, rsa_4096_oaep_sha256, rsa_4096_oaep_sha512, rsa_2048_oaep_sha256_aes_256_key_wrap, rsa_3072_oaep_sha256_aes_256_key_wrap, rsa_4096_oaep_sha256_aes_256_key_wrap, rsa_4096_oaep_sha512_aes_256_key_wrap]) + --description string Optional description of the wrapping key + -h, --help Help for "stackit kms wrapping-key create" + --keyring-id string ID of the KMS key ring + --name string The display name to distinguish multiple wrapping keys + --protection string The underlying system that is responsible for protecting the key material. (one of: [software]) + --purpose string Purpose of the key. (one of: [wrap_symmetric_key, wrap_asymmetric_key]) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms wrapping-key](./stackit_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/docs/stackit_kms_wrapping-key_delete.md b/docs/stackit_kms_wrapping-key_delete.md new file mode 100644 index 000000000..e1346d0ce --- /dev/null +++ b/docs/stackit_kms_wrapping-key_delete.md @@ -0,0 +1,41 @@ +## stackit kms wrapping-key delete + +Deletes a KMS wrapping key + +### Synopsis + +Deletes a KMS wrapping key inside a specific key ring. + +``` +stackit kms wrapping-key delete WRAPPING_KEY_ID [flags] +``` + +### Examples + +``` + Delete a KMS wrapping key "MY_WRAPPING_KEY_ID" inside the key ring "my-keyring-id" + $ stackit kms wrapping-key delete "MY_WRAPPING_KEY_ID" --keyring-id "my-keyring-id" +``` + +### Options + +``` + -h, --help Help for "stackit kms wrapping-key delete" + --keyring-id string ID of the KMS key ring where the wrapping key is stored +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms wrapping-key](./stackit_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/docs/stackit_kms_wrapping-key_describe.md b/docs/stackit_kms_wrapping-key_describe.md new file mode 100644 index 000000000..da0a587f9 --- /dev/null +++ b/docs/stackit_kms_wrapping-key_describe.md @@ -0,0 +1,41 @@ +## stackit kms wrapping-key describe + +Describe a KMS wrapping key + +### Synopsis + +Describe a KMS wrapping key + +``` +stackit kms wrapping-key describe WRAPPING_KEY_ID [flags] +``` + +### Examples + +``` + Describe a KMS wrapping key with ID xxx of keyring yyy + $ stackit kms wrapping-key describe xxx --keyring-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit kms wrapping-key describe" + --keyring-id string Key Ring ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms wrapping-key](./stackit_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/docs/stackit_kms_wrapping-key_list.md b/docs/stackit_kms_wrapping-key_list.md new file mode 100644 index 000000000..9d33332bd --- /dev/null +++ b/docs/stackit_kms_wrapping-key_list.md @@ -0,0 +1,44 @@ +## stackit kms wrapping-key list + +Lists all KMS wrapping keys + +### Synopsis + +Lists all KMS wrapping keys inside a key ring. + +``` +stackit kms wrapping-key list [flags] +``` + +### Examples + +``` + List all KMS wrapping keys for the key ring "my-keyring-id" + $ stackit kms wrapping-key list --keyring-id "my-keyring-id" + + List all KMS wrapping keys in JSON format + $ stackit kms wrapping-key list --keyring-id "my-keyring-id" --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit kms wrapping-key list" + --keyring-id string ID of the KMS key ring where the key is stored +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit kms wrapping-key](./stackit_kms_wrapping-key.md) - Manage KMS wrapping keys + diff --git a/docs/stackit_load-balancer.md b/docs/stackit_load-balancer.md index 77c14b00f..d320049b9 100644 --- a/docs/stackit_load-balancer.md +++ b/docs/stackit_load-balancer.md @@ -21,10 +21,10 @@ stackit load-balancer [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_create.md b/docs/stackit_load-balancer_create.md index 4172ccb4d..a76a826e0 100644 --- a/docs/stackit_load-balancer_create.md +++ b/docs/stackit_load-balancer_create.md @@ -6,7 +6,7 @@ Creates a Load Balancer Creates a Load Balancer. The payload can be provided as a JSON string or a file path prefixed with "@". -See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure. +See https://docs.api.stackit.cloud/documentation/load-balancer/version/v2#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure. ``` stackit load-balancer create [flags] @@ -39,10 +39,10 @@ stackit load-balancer create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_delete.md b/docs/stackit_load-balancer_delete.md index 07ee2712c..79ef95344 100644 --- a/docs/stackit_load-balancer_delete.md +++ b/docs/stackit_load-balancer_delete.md @@ -28,10 +28,10 @@ stackit load-balancer delete LOAD_BALANCER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_describe.md b/docs/stackit_load-balancer_describe.md index 8f3bd5da1..42abb7dee 100644 --- a/docs/stackit_load-balancer_describe.md +++ b/docs/stackit_load-balancer_describe.md @@ -31,10 +31,10 @@ stackit load-balancer describe LOAD_BALANCER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_generate-payload.md b/docs/stackit_load-balancer_generate-payload.md index 2cf2b15c7..44291dc38 100644 --- a/docs/stackit_load-balancer_generate-payload.md +++ b/docs/stackit_load-balancer_generate-payload.md @@ -5,7 +5,7 @@ Generates a payload to create/update a Load Balancer ### Synopsis Generates a JSON payload with values to be used as --payload input for load balancer creation or update. -See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure. +See https://docs.api.stackit.cloud/documentation/load-balancer/version/v2#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure. ``` stackit load-balancer generate-payload [flags] @@ -41,10 +41,10 @@ stackit load-balancer generate-payload [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_list.md b/docs/stackit_load-balancer_list.md index 3cc3749e9..1eefe0b1a 100644 --- a/docs/stackit_load-balancer_list.md +++ b/docs/stackit_load-balancer_list.md @@ -35,10 +35,10 @@ stackit load-balancer list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials.md b/docs/stackit_load-balancer_observability-credentials.md index ae2e58a8e..c6dd0c4bc 100644 --- a/docs/stackit_load-balancer_observability-credentials.md +++ b/docs/stackit_load-balancer_observability-credentials.md @@ -21,10 +21,10 @@ stackit load-balancer observability-credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials_add.md b/docs/stackit_load-balancer_observability-credentials_add.md index 97afccbf4..f5164bf93 100644 --- a/docs/stackit_load-balancer_observability-credentials_add.md +++ b/docs/stackit_load-balancer_observability-credentials_add.md @@ -25,7 +25,7 @@ stackit load-balancer observability-credentials add [flags] ``` --display-name string Credentials display name -h, --help Help for "stackit load-balancer observability-credentials add" - --password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt). + --password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. --username string Username ``` @@ -34,10 +34,10 @@ stackit load-balancer observability-credentials add [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials_cleanup.md b/docs/stackit_load-balancer_observability-credentials_cleanup.md index 58fcbe82e..e873c9b51 100644 --- a/docs/stackit_load-balancer_observability-credentials_cleanup.md +++ b/docs/stackit_load-balancer_observability-credentials_cleanup.md @@ -28,10 +28,10 @@ stackit load-balancer observability-credentials cleanup [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials_delete.md b/docs/stackit_load-balancer_observability-credentials_delete.md index a2fcf8018..e94be6d34 100644 --- a/docs/stackit_load-balancer_observability-credentials_delete.md +++ b/docs/stackit_load-balancer_observability-credentials_delete.md @@ -28,10 +28,10 @@ stackit load-balancer observability-credentials delete CREDENTIALS_REF [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials_describe.md b/docs/stackit_load-balancer_observability-credentials_describe.md index c8ed19750..38602a204 100644 --- a/docs/stackit_load-balancer_observability-credentials_describe.md +++ b/docs/stackit_load-balancer_observability-credentials_describe.md @@ -28,10 +28,10 @@ stackit load-balancer observability-credentials describe CREDENTIALS_REF [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials_list.md b/docs/stackit_load-balancer_observability-credentials_list.md index 2e86129d0..14e7599f3 100644 --- a/docs/stackit_load-balancer_observability-credentials_list.md +++ b/docs/stackit_load-balancer_observability-credentials_list.md @@ -43,10 +43,10 @@ stackit load-balancer observability-credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_observability-credentials_update.md b/docs/stackit_load-balancer_observability-credentials_update.md index c0d95a31d..1e19bd14d 100644 --- a/docs/stackit_load-balancer_observability-credentials_update.md +++ b/docs/stackit_load-balancer_observability-credentials_update.md @@ -25,7 +25,7 @@ stackit load-balancer observability-credentials update CREDENTIALS_REF [flags] ``` --display-name string Credentials name -h, --help Help for "stackit load-balancer observability-credentials update" - --password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt). + --password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. --username string Username ``` @@ -34,10 +34,10 @@ stackit load-balancer observability-credentials update CREDENTIALS_REF [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_quota.md b/docs/stackit_load-balancer_quota.md index 62541ec3e..15864f7fe 100644 --- a/docs/stackit_load-balancer_quota.md +++ b/docs/stackit_load-balancer_quota.md @@ -28,10 +28,10 @@ stackit load-balancer quota [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_target-pool.md b/docs/stackit_load-balancer_target-pool.md index 8356f0436..47d7753cd 100644 --- a/docs/stackit_load-balancer_target-pool.md +++ b/docs/stackit_load-balancer_target-pool.md @@ -21,10 +21,10 @@ stackit load-balancer target-pool [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_target-pool_add-target.md b/docs/stackit_load-balancer_target-pool_add-target.md index b6e1e8109..157375bed 100644 --- a/docs/stackit_load-balancer_target-pool_add-target.md +++ b/docs/stackit_load-balancer_target-pool_add-target.md @@ -32,10 +32,10 @@ stackit load-balancer target-pool add-target TARGET_IP [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_target-pool_describe.md b/docs/stackit_load-balancer_target-pool_describe.md index 67fbe0580..a4b663fd9 100644 --- a/docs/stackit_load-balancer_target-pool_describe.md +++ b/docs/stackit_load-balancer_target-pool_describe.md @@ -32,10 +32,10 @@ stackit load-balancer target-pool describe TARGET_POOL_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_target-pool_remove-target.md b/docs/stackit_load-balancer_target-pool_remove-target.md index 2d95b7abf..cf4ec0a70 100644 --- a/docs/stackit_load-balancer_target-pool_remove-target.md +++ b/docs/stackit_load-balancer_target-pool_remove-target.md @@ -30,10 +30,10 @@ stackit load-balancer target-pool remove-target TARGET_IP [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_load-balancer_update.md b/docs/stackit_load-balancer_update.md index fc577b35b..98e18d6cf 100644 --- a/docs/stackit_load-balancer_update.md +++ b/docs/stackit_load-balancer_update.md @@ -6,7 +6,7 @@ Updates a Load Balancer Updates a load balancer. The payload can be provided as a JSON string or a file path prefixed with "@". -See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_UpdateLoadBalancer for information regarding the payload structure. +See https://docs.api.stackit.cloud/documentation/load-balancer/version/v2#tag/Load-Balancer/operation/APIService_UpdateLoadBalancer for information regarding the payload structure. ``` stackit load-balancer update LOAD_BALANCER_NAME [flags] @@ -39,10 +39,10 @@ stackit load-balancer update LOAD_BALANCER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme.md b/docs/stackit_logme.md index edd1ec1e7..71fde4711 100644 --- a/docs/stackit_logme.md +++ b/docs/stackit_logme.md @@ -21,10 +21,10 @@ stackit logme [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_credentials.md b/docs/stackit_logme_credentials.md index f510d9854..3044a375d 100644 --- a/docs/stackit_logme_credentials.md +++ b/docs/stackit_logme_credentials.md @@ -21,10 +21,10 @@ stackit logme credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_credentials_create.md b/docs/stackit_logme_credentials_create.md index f20a9c583..7203c396f 100644 --- a/docs/stackit_logme_credentials_create.md +++ b/docs/stackit_logme_credentials_create.md @@ -33,10 +33,10 @@ stackit logme credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_credentials_delete.md b/docs/stackit_logme_credentials_delete.md index 0925c4cbd..5ed60f9eb 100644 --- a/docs/stackit_logme_credentials_delete.md +++ b/docs/stackit_logme_credentials_delete.md @@ -29,10 +29,10 @@ stackit logme credentials delete CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_credentials_describe.md b/docs/stackit_logme_credentials_describe.md index 96940297b..b6d79f7ff 100644 --- a/docs/stackit_logme_credentials_describe.md +++ b/docs/stackit_logme_credentials_describe.md @@ -32,10 +32,10 @@ stackit logme credentials describe CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_credentials_list.md b/docs/stackit_logme_credentials_list.md index 3cd2a5164..9dac8453e 100644 --- a/docs/stackit_logme_credentials_list.md +++ b/docs/stackit_logme_credentials_list.md @@ -36,10 +36,10 @@ stackit logme credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_instance.md b/docs/stackit_logme_instance.md index 54144eb80..7de2cf05e 100644 --- a/docs/stackit_logme_instance.md +++ b/docs/stackit_logme_instance.md @@ -21,10 +21,10 @@ stackit logme instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_instance_create.md b/docs/stackit_logme_instance_create.md index a0af8584d..cf71af87a 100644 --- a/docs/stackit_logme_instance_create.md +++ b/docs/stackit_logme_instance_create.md @@ -30,7 +30,7 @@ stackit logme instance create [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit logme instance create" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID -n, --name string Instance name @@ -45,10 +45,10 @@ stackit logme instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_instance_delete.md b/docs/stackit_logme_instance_delete.md index 34e9a8fc9..aed7fab3e 100644 --- a/docs/stackit_logme_instance_delete.md +++ b/docs/stackit_logme_instance_delete.md @@ -28,10 +28,10 @@ stackit logme instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_instance_describe.md b/docs/stackit_logme_instance_describe.md index c9f0f9bcc..3c86c04ce 100644 --- a/docs/stackit_logme_instance_describe.md +++ b/docs/stackit_logme_instance_describe.md @@ -31,10 +31,10 @@ stackit logme instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_instance_list.md b/docs/stackit_logme_instance_list.md index 20673a368..04ab54703 100644 --- a/docs/stackit_logme_instance_list.md +++ b/docs/stackit_logme_instance_list.md @@ -35,10 +35,10 @@ stackit logme instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_instance_update.md b/docs/stackit_logme_instance_update.md index 02e3bb975..11e3c6963 100644 --- a/docs/stackit_logme_instance_update.md +++ b/docs/stackit_logme_instance_update.md @@ -27,9 +27,10 @@ stackit logme instance update INSTANCE_ID [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit logme instance update" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID + -n, --name string Instance name --plan-id string Plan ID --plan-name string Plan name --syslog strings Syslog @@ -41,10 +42,10 @@ stackit logme instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logme_plans.md b/docs/stackit_logme_plans.md index 87f7db2a3..cc44ce3b4 100644 --- a/docs/stackit_logme_plans.md +++ b/docs/stackit_logme_plans.md @@ -35,10 +35,10 @@ stackit logme plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_logs.md b/docs/stackit_logs.md new file mode 100644 index 000000000..a1cea5280 --- /dev/null +++ b/docs/stackit_logs.md @@ -0,0 +1,35 @@ +## stackit logs + +Provides functionality for Logs + +### Synopsis + +Provides functionality for Logs. + +``` +stackit logs [flags] +``` + +### Options + +``` + -h, --help Help for "stackit logs" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens +* [stackit logs instance](./stackit_logs_instance.md) - Provides functionality for Logs instances + diff --git a/docs/stackit_logs_access-token.md b/docs/stackit_logs_access-token.md new file mode 100644 index 000000000..6c8edc0a6 --- /dev/null +++ b/docs/stackit_logs_access-token.md @@ -0,0 +1,40 @@ +## stackit logs access-token + +Provides functionality for Logs access-tokens + +### Synopsis + +Provides functionality for Logs access-tokens. + +``` +stackit logs access-token [flags] +``` + +### Options + +``` + -h, --help Help for "stackit logs access-token" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs](./stackit_logs.md) - Provides functionality for Logs +* [stackit logs access-token create](./stackit_logs_access-token_create.md) - Creates a Logs access token +* [stackit logs access-token delete](./stackit_logs_access-token_delete.md) - Deletes a Logs access token +* [stackit logs access-token delete-all](./stackit_logs_access-token_delete-all.md) - Deletes all Logs access token +* [stackit logs access-token delete-all-expired](./stackit_logs_access-token_delete-all-expired.md) - Deletes all expired Logs access token +* [stackit logs access-token describe](./stackit_logs_access-token_describe.md) - Shows details of a Logs access token +* [stackit logs access-token list](./stackit_logs_access-token_list.md) - Lists all Logs access tokens of a project +* [stackit logs access-token update](./stackit_logs_access-token_update.md) - Updates a Logs access token + diff --git a/docs/stackit_logs_access-token_create.md b/docs/stackit_logs_access-token_create.md new file mode 100644 index 000000000..6c6a3a73d --- /dev/null +++ b/docs/stackit_logs_access-token_create.md @@ -0,0 +1,51 @@ +## stackit logs access-token create + +Creates a Logs access token + +### Synopsis + +Creates a Logs access token. + +``` +stackit logs access-token create [flags] +``` + +### Examples + +``` + Create a access token with the display name "access-token-1" for the instance "xxx" with read and write permissions + $ stackit logs access-token create --display-name access-token-1 --instance-id xxx --permissions read,write + + Create a write only access token with a description + $ stackit logs access-token create --display-name access-token-2 --instance-id xxx --permissions write --description "Access token for service" + + Create a read only access token which expires in 30 days + $ stackit logs access-token create --display-name access-token-3 --instance-id xxx --permissions read --lifetime 30 +``` + +### Options + +``` + --description string Description of the access token + --display-name string Display name for the access token + -h, --help Help for "stackit logs access-token create" + --instance-id string ID of the Logs instance + --lifetime int32 Lifetime of the access token in days [1 - 180] + --permissions strings Permissions of the access token ["read" "write"] +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_access-token_delete-all-expired.md b/docs/stackit_logs_access-token_delete-all-expired.md new file mode 100644 index 000000000..31e4ac22e --- /dev/null +++ b/docs/stackit_logs_access-token_delete-all-expired.md @@ -0,0 +1,41 @@ +## stackit logs access-token delete-all-expired + +Deletes all expired Logs access token + +### Synopsis + +Deletes all expired Logs access token. + +``` +stackit logs access-token delete-all-expired [flags] +``` + +### Examples + +``` + Delete all expired access tokens in instance "xxx" + $ stackit logs access-token delete-all-expired --instance-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit logs access-token delete-all-expired" + --instance-id string ID of the Logs instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_access-token_delete-all.md b/docs/stackit_logs_access-token_delete-all.md new file mode 100644 index 000000000..ce7fbe835 --- /dev/null +++ b/docs/stackit_logs_access-token_delete-all.md @@ -0,0 +1,41 @@ +## stackit logs access-token delete-all + +Deletes all Logs access token + +### Synopsis + +Deletes all Logs access token. + +``` +stackit logs access-token delete-all [flags] +``` + +### Examples + +``` + Delete all access tokens in instance "xxx" + $ stackit logs access-token delete-all --instance-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit logs access-token delete-all" + --instance-id string ID of the Logs instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_access-token_delete.md b/docs/stackit_logs_access-token_delete.md new file mode 100644 index 000000000..1af40862e --- /dev/null +++ b/docs/stackit_logs_access-token_delete.md @@ -0,0 +1,41 @@ +## stackit logs access-token delete + +Deletes a Logs access token + +### Synopsis + +Deletes a Logs access token. + +``` +stackit logs access-token delete ACCESS_TOKEN_ID [flags] +``` + +### Examples + +``` + Delete access token with ID "xxx" in instance "yyy" + $ stackit logs access-token delete xxx --instance-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit logs access-token delete" + --instance-id string ID of the Logs instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_access-token_describe.md b/docs/stackit_logs_access-token_describe.md new file mode 100644 index 000000000..24cb0ae33 --- /dev/null +++ b/docs/stackit_logs_access-token_describe.md @@ -0,0 +1,44 @@ +## stackit logs access-token describe + +Shows details of a Logs access token + +### Synopsis + +Shows details of a Logs access token. + +``` +stackit logs access-token describe ACCESS_TOKEN_ID [flags] +``` + +### Examples + +``` + Show details of a Logs access token with ID "xxx" + $ stackit logs access-token describe xxx + + Show details of a Logs access token with ID "xxx" in JSON format + $ stackit logs access-token describe xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit logs access-token describe" + --instance-id string ID of the Logs instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_access-token_list.md b/docs/stackit_logs_access-token_list.md new file mode 100644 index 000000000..ba26517e8 --- /dev/null +++ b/docs/stackit_logs_access-token_list.md @@ -0,0 +1,48 @@ +## stackit logs access-token list + +Lists all Logs access tokens of a project + +### Synopsis + +Lists all access tokens of a project. + +``` +stackit logs access-token list [flags] +``` + +### Examples + +``` + Lists all access tokens of the instance "xxx" + $ stackit logs access-token list --instance-id xxx + + Lists all access tokens in JSON format + $ stackit logs access-token list --instance-id xxx --output-format json + + Lists up to 10 access-token + $ stackit logs access-token list --instance-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit logs access-token list" + --instance-id string ID of the Logs instance + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_access-token_update.md b/docs/stackit_logs_access-token_update.md new file mode 100644 index 000000000..d6e218261 --- /dev/null +++ b/docs/stackit_logs_access-token_update.md @@ -0,0 +1,46 @@ +## stackit logs access-token update + +Updates a Logs access token + +### Synopsis + +Updates a access token. + +``` +stackit logs access-token update ACCESS_TOKEN_ID [flags] +``` + +### Examples + +``` + Update access token with ID "xxx" with new name "access-token-1" + $ stackit logs access-token update xxx --instance-id yyy --display-name access-token-1 + + Update access token with ID "xxx" with new description "Access token for Service XY" + $ stackit logs access-token update xxx --instance-id yyy --description "Access token for Service XY" +``` + +### Options + +``` + --description string Description of the access token + --display-name string Display name for the access token + -h, --help Help for "stackit logs access-token update" + --instance-id string ID of the Logs instance +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs access-token](./stackit_logs_access-token.md) - Provides functionality for Logs access-tokens + diff --git a/docs/stackit_logs_instance.md b/docs/stackit_logs_instance.md new file mode 100644 index 000000000..75a00812a --- /dev/null +++ b/docs/stackit_logs_instance.md @@ -0,0 +1,38 @@ +## stackit logs instance + +Provides functionality for Logs instances + +### Synopsis + +Provides functionality for Logs instances. + +``` +stackit logs instance [flags] +``` + +### Options + +``` + -h, --help Help for "stackit logs instance" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs](./stackit_logs.md) - Provides functionality for Logs +* [stackit logs instance create](./stackit_logs_instance_create.md) - Creates a Logs instance +* [stackit logs instance delete](./stackit_logs_instance_delete.md) - Deletes the given Logs instance +* [stackit logs instance describe](./stackit_logs_instance_describe.md) - Shows details of a Logs instance +* [stackit logs instance list](./stackit_logs_instance_list.md) - Lists Logs instances +* [stackit logs instance update](./stackit_logs_instance_update.md) - Updates a Logs instance + diff --git a/docs/stackit_logs_instance_create.md b/docs/stackit_logs_instance_create.md new file mode 100644 index 000000000..cee96d60f --- /dev/null +++ b/docs/stackit_logs_instance_create.md @@ -0,0 +1,50 @@ +## stackit logs instance create + +Creates a Logs instance + +### Synopsis + +Creates a Logs instance. + +``` +stackit logs instance create [flags] +``` + +### Examples + +``` + Create a Logs instance with name "my-instance" and retention time 10 days + $ stackit logs instance create --display-name "my-instance" --retention-days 10 + + Create a Logs instance with name "my-instance", retention time 10 days, and a description + $ stackit logs instance create --display-name "my-instance" --retention-days 10 --description "Description of the instance" + + Create a Logs instance with name "my-instance", retention time 10 days, and restrict access to a specific range of IP addresses. + $ stackit logs instance create --display-name "my-instance" --retention-days 10 --acl 1.2.3.0/24 +``` + +### Options + +``` + --acl strings Access control list + --description string Description + --display-name string Display name + -h, --help Help for "stackit logs instance create" + --retention-days int32 The days for how long the logs should be stored before being cleaned up +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs instance](./stackit_logs_instance.md) - Provides functionality for Logs instances + diff --git a/docs/stackit_logs_instance_delete.md b/docs/stackit_logs_instance_delete.md new file mode 100644 index 000000000..64e60b3e1 --- /dev/null +++ b/docs/stackit_logs_instance_delete.md @@ -0,0 +1,40 @@ +## stackit logs instance delete + +Deletes the given Logs instance + +### Synopsis + +Deletes the given Logs instance. + +``` +stackit logs instance delete INSTANCE_ID [flags] +``` + +### Examples + +``` + Delete a Logs instance with ID "xxx" + $ stackit logs instance delete "xxx" +``` + +### Options + +``` + -h, --help Help for "stackit logs instance delete" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs instance](./stackit_logs_instance.md) - Provides functionality for Logs instances + diff --git a/docs/stackit_logs_instance_describe.md b/docs/stackit_logs_instance_describe.md new file mode 100644 index 000000000..ceccab5ce --- /dev/null +++ b/docs/stackit_logs_instance_describe.md @@ -0,0 +1,43 @@ +## stackit logs instance describe + +Shows details of a Logs instance + +### Synopsis + +Shows details of a Logs instance + +``` +stackit logs instance describe INSTANCE_ID [flags] +``` + +### Examples + +``` + Get details of a Logs instance with ID "xxx" + $ stackit logs instance describe xxx + + Get details of a Logs instance with ID "xxx" in JSON format + $ stackit logs instance describe xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit logs instance describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs instance](./stackit_logs_instance.md) - Provides functionality for Logs instances + diff --git a/docs/stackit_logs_instance_list.md b/docs/stackit_logs_instance_list.md new file mode 100644 index 000000000..5a35c2e10 --- /dev/null +++ b/docs/stackit_logs_instance_list.md @@ -0,0 +1,44 @@ +## stackit logs instance list + +Lists Logs instances + +### Synopsis + +Lists Logs instances within the project. + +``` +stackit logs instance list [flags] +``` + +### Examples + +``` + List all Logs instances + $ stackit logs instance list + + List the first 10 Logs instances + $ stackit logs instance list --limit=10 +``` + +### Options + +``` + -h, --help Help for "stackit logs instance list" + --limit int Limit the output to the first n elements +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs instance](./stackit_logs_instance.md) - Provides functionality for Logs instances + diff --git a/docs/stackit_logs_instance_update.md b/docs/stackit_logs_instance_update.md new file mode 100644 index 000000000..877903b3b --- /dev/null +++ b/docs/stackit_logs_instance_update.md @@ -0,0 +1,50 @@ +## stackit logs instance update + +Updates a Logs instance + +### Synopsis + +Updates a Logs instance. + +``` +stackit logs instance update INSTANCE_ID [flags] +``` + +### Examples + +``` + Update the display name of the Logs instance with ID "xxx" + $ stackit logs instance update xxx --display-name new-name + + Update the retention time of the Logs instance with ID "xxx" + $ stackit logs instance update xxx --retention-days 40 + + Update the ACL of the Logs instance with ID "xxx" + $ stackit logs instance update xxx --acl 1.2.3.0/24 +``` + +### Options + +``` + --acl strings Access control list + --description string Description + --display-name string Display name + -h, --help Help for "stackit logs instance update" + --retention-days int32 The days for how long the logs should be stored before being cleaned up +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit logs instance](./stackit_logs_instance.md) - Provides functionality for Logs instances + diff --git a/docs/stackit_mariadb.md b/docs/stackit_mariadb.md index b40107270..7638f6ce0 100644 --- a/docs/stackit_mariadb.md +++ b/docs/stackit_mariadb.md @@ -21,10 +21,10 @@ stackit mariadb [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_credentials.md b/docs/stackit_mariadb_credentials.md index ccfa3e470..42b3ae183 100644 --- a/docs/stackit_mariadb_credentials.md +++ b/docs/stackit_mariadb_credentials.md @@ -21,10 +21,10 @@ stackit mariadb credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_credentials_create.md b/docs/stackit_mariadb_credentials_create.md index e611ebeff..ff3a9ac84 100644 --- a/docs/stackit_mariadb_credentials_create.md +++ b/docs/stackit_mariadb_credentials_create.md @@ -33,10 +33,10 @@ stackit mariadb credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_credentials_delete.md b/docs/stackit_mariadb_credentials_delete.md index e1b7bbee7..a8cc050b3 100644 --- a/docs/stackit_mariadb_credentials_delete.md +++ b/docs/stackit_mariadb_credentials_delete.md @@ -29,10 +29,10 @@ stackit mariadb credentials delete CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_credentials_describe.md b/docs/stackit_mariadb_credentials_describe.md index 12f440607..5099afaf7 100644 --- a/docs/stackit_mariadb_credentials_describe.md +++ b/docs/stackit_mariadb_credentials_describe.md @@ -32,10 +32,10 @@ stackit mariadb credentials describe CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_credentials_list.md b/docs/stackit_mariadb_credentials_list.md index 99120fda8..7cf303afa 100644 --- a/docs/stackit_mariadb_credentials_list.md +++ b/docs/stackit_mariadb_credentials_list.md @@ -36,10 +36,10 @@ stackit mariadb credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_instance.md b/docs/stackit_mariadb_instance.md index 3ace44fc6..d40fbdf1a 100644 --- a/docs/stackit_mariadb_instance.md +++ b/docs/stackit_mariadb_instance.md @@ -21,10 +21,10 @@ stackit mariadb instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_instance_create.md b/docs/stackit_mariadb_instance_create.md index 63bb11865..2fc227325 100644 --- a/docs/stackit_mariadb_instance_create.md +++ b/docs/stackit_mariadb_instance_create.md @@ -30,7 +30,7 @@ stackit mariadb instance create [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit mariadb instance create" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID -n, --name string Instance name @@ -45,10 +45,10 @@ stackit mariadb instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_instance_delete.md b/docs/stackit_mariadb_instance_delete.md index 39ea03e35..6dbb5b29c 100644 --- a/docs/stackit_mariadb_instance_delete.md +++ b/docs/stackit_mariadb_instance_delete.md @@ -28,10 +28,10 @@ stackit mariadb instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_instance_describe.md b/docs/stackit_mariadb_instance_describe.md index e864c29a6..7937de78b 100644 --- a/docs/stackit_mariadb_instance_describe.md +++ b/docs/stackit_mariadb_instance_describe.md @@ -31,10 +31,10 @@ stackit mariadb instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_instance_list.md b/docs/stackit_mariadb_instance_list.md index 2990fbc24..640bebf97 100644 --- a/docs/stackit_mariadb_instance_list.md +++ b/docs/stackit_mariadb_instance_list.md @@ -35,10 +35,10 @@ stackit mariadb instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_instance_update.md b/docs/stackit_mariadb_instance_update.md index 2de6c1026..8032fc982 100644 --- a/docs/stackit_mariadb_instance_update.md +++ b/docs/stackit_mariadb_instance_update.md @@ -27,7 +27,7 @@ stackit mariadb instance update INSTANCE_ID [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit mariadb instance update" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID --plan-id string Plan ID @@ -41,10 +41,10 @@ stackit mariadb instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mariadb_plans.md b/docs/stackit_mariadb_plans.md index f17b09ecd..698f15e80 100644 --- a/docs/stackit_mariadb_plans.md +++ b/docs/stackit_mariadb_plans.md @@ -35,10 +35,10 @@ stackit mariadb plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex.md b/docs/stackit_mongodbflex.md index 7f0746976..2d741af26 100644 --- a/docs/stackit_mongodbflex.md +++ b/docs/stackit_mongodbflex.md @@ -21,10 +21,10 @@ stackit mongodbflex [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup.md b/docs/stackit_mongodbflex_backup.md index e89b79ae5..e837c07c8 100644 --- a/docs/stackit_mongodbflex_backup.md +++ b/docs/stackit_mongodbflex_backup.md @@ -21,10 +21,10 @@ stackit mongodbflex backup [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup_describe.md b/docs/stackit_mongodbflex_backup_describe.md index 287f325b4..fbf984060 100644 --- a/docs/stackit_mongodbflex_backup_describe.md +++ b/docs/stackit_mongodbflex_backup_describe.md @@ -32,10 +32,10 @@ stackit mongodbflex backup describe BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup_list.md b/docs/stackit_mongodbflex_backup_list.md index 87fa406ea..ac975f3ab 100644 --- a/docs/stackit_mongodbflex_backup_list.md +++ b/docs/stackit_mongodbflex_backup_list.md @@ -36,10 +36,10 @@ stackit mongodbflex backup list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup_restore-jobs.md b/docs/stackit_mongodbflex_backup_restore-jobs.md index c91bcaf2f..95be4e8b8 100644 --- a/docs/stackit_mongodbflex_backup_restore-jobs.md +++ b/docs/stackit_mongodbflex_backup_restore-jobs.md @@ -36,10 +36,10 @@ stackit mongodbflex backup restore-jobs [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup_restore.md b/docs/stackit_mongodbflex_backup_restore.md index b506f1ce6..127f11dc1 100644 --- a/docs/stackit_mongodbflex_backup_restore.md +++ b/docs/stackit_mongodbflex_backup_restore.md @@ -40,10 +40,10 @@ stackit mongodbflex backup restore [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup_schedule.md b/docs/stackit_mongodbflex_backup_schedule.md index fff1ef32c..befd7ee44 100644 --- a/docs/stackit_mongodbflex_backup_schedule.md +++ b/docs/stackit_mongodbflex_backup_schedule.md @@ -32,10 +32,10 @@ stackit mongodbflex backup schedule [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_backup_update-schedule.md b/docs/stackit_mongodbflex_backup_update-schedule.md index cfa492a84..0d8f83731 100644 --- a/docs/stackit_mongodbflex_backup_update-schedule.md +++ b/docs/stackit_mongodbflex_backup_update-schedule.md @@ -40,10 +40,10 @@ stackit mongodbflex backup update-schedule [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_instance.md b/docs/stackit_mongodbflex_instance.md index 962f4bc8e..9c85c8f34 100644 --- a/docs/stackit_mongodbflex_instance.md +++ b/docs/stackit_mongodbflex_instance.md @@ -21,10 +21,10 @@ stackit mongodbflex instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_instance_create.md b/docs/stackit_mongodbflex_instance_create.md index 151ff2dad..13b67ea65 100644 --- a/docs/stackit_mongodbflex_instance_create.md +++ b/docs/stackit_mongodbflex_instance_create.md @@ -28,14 +28,14 @@ stackit mongodbflex instance create [flags] ``` --acl strings The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc. (default []) --backup-schedule string Backup schedule (default "0 0/6 * * *") - --cpu int Number of CPUs + --cpu int32 Number of CPUs --flavor-id string ID of the flavor -h, --help Help for "stackit mongodbflex instance create" -n, --name string Instance name - --ram int Amount of RAM (in GB) + --ram int32 Amount of RAM (in GB) --storage-class string Storage class (default "premium-perf2-mongodb") --storage-size int Storage size (in GB) (default 10) - --type string Instance type, one of ["Replica" "Sharded" "Single"] (default "Replica") + --type string Instance type, (one of: [Replica, Sharded, Single]) (default "Replica") --version string MongoDB version. Defaults to the latest version available ``` @@ -44,10 +44,10 @@ stackit mongodbflex instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_instance_delete.md b/docs/stackit_mongodbflex_instance_delete.md index 39d313068..44a50e866 100644 --- a/docs/stackit_mongodbflex_instance_delete.md +++ b/docs/stackit_mongodbflex_instance_delete.md @@ -28,10 +28,10 @@ stackit mongodbflex instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_instance_describe.md b/docs/stackit_mongodbflex_instance_describe.md index b37bd3605..5910be339 100644 --- a/docs/stackit_mongodbflex_instance_describe.md +++ b/docs/stackit_mongodbflex_instance_describe.md @@ -31,10 +31,10 @@ stackit mongodbflex instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_instance_list.md b/docs/stackit_mongodbflex_instance_list.md index 5abfe9cbc..57535fcaf 100644 --- a/docs/stackit_mongodbflex_instance_list.md +++ b/docs/stackit_mongodbflex_instance_list.md @@ -35,10 +35,10 @@ stackit mongodbflex instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_instance_update.md b/docs/stackit_mongodbflex_instance_update.md index a9475fbeb..44921ddd8 100644 --- a/docs/stackit_mongodbflex_instance_update.md +++ b/docs/stackit_mongodbflex_instance_update.md @@ -25,14 +25,14 @@ stackit mongodbflex instance update INSTANCE_ID [flags] ``` --acl strings Lists of IP networks in CIDR notation which are allowed to access this instance (default []) --backup-schedule string Backup schedule - --cpu int Number of CPUs + --cpu int32 Number of CPUs --flavor-id string ID of the flavor -h, --help Help for "stackit mongodbflex instance update" -n, --name string Instance name - --ram int Amount of RAM (in GB) + --ram int32 Amount of RAM (in GB) --storage-class string Storage class --storage-size int Storage size (in GB) - --type string Instance type, one of ["Replica" "Sharded" "Single"] + --type string Instance type, (one of: [Replica, Sharded, Single]) --version string Version ``` @@ -41,10 +41,10 @@ stackit mongodbflex instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_options.md b/docs/stackit_mongodbflex_options.md index d01544608..31ad5fa7f 100644 --- a/docs/stackit_mongodbflex_options.md +++ b/docs/stackit_mongodbflex_options.md @@ -39,10 +39,10 @@ stackit mongodbflex options [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user.md b/docs/stackit_mongodbflex_user.md index ce6528aa4..f1a53b480 100644 --- a/docs/stackit_mongodbflex_user.md +++ b/docs/stackit_mongodbflex_user.md @@ -21,10 +21,10 @@ stackit mongodbflex user [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user_create.md b/docs/stackit_mongodbflex_user_create.md index 99075fee8..17a3648bb 100644 --- a/docs/stackit_mongodbflex_user_create.md +++ b/docs/stackit_mongodbflex_user_create.md @@ -29,7 +29,7 @@ stackit mongodbflex user create [flags] --database string The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it -h, --help Help for "stackit mongodbflex user create" --instance-id string ID of the instance - --role strings Roles of the user, possible values are ["read" "readWrite" "readWriteAnyDatabase"] (default [read]) + --role strings Roles of the user. The "readAnyDatabase", "readWriteAnyDatabase" and "stackitAdmin" roles will always be created in the admin database. (multiple of: [read, readWrite, readAnyDatabase, readWriteAnyDatabase, stackitAdmin]) (default [read]) --username string Username of the user. If not specified, a random username will be assigned ``` @@ -38,10 +38,10 @@ stackit mongodbflex user create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user_delete.md b/docs/stackit_mongodbflex_user_delete.md index bf792ddea..08262bbf8 100644 --- a/docs/stackit_mongodbflex_user_delete.md +++ b/docs/stackit_mongodbflex_user_delete.md @@ -30,10 +30,10 @@ stackit mongodbflex user delete USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user_describe.md b/docs/stackit_mongodbflex_user_describe.md index 15642c9c2..6bca8c786 100644 --- a/docs/stackit_mongodbflex_user_describe.md +++ b/docs/stackit_mongodbflex_user_describe.md @@ -34,10 +34,10 @@ stackit mongodbflex user describe USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user_list.md b/docs/stackit_mongodbflex_user_list.md index 804abfe11..49327cb1e 100644 --- a/docs/stackit_mongodbflex_user_list.md +++ b/docs/stackit_mongodbflex_user_list.md @@ -36,10 +36,10 @@ stackit mongodbflex user list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user_reset-password.md b/docs/stackit_mongodbflex_user_reset-password.md index 183885b5f..b87591eba 100644 --- a/docs/stackit_mongodbflex_user_reset-password.md +++ b/docs/stackit_mongodbflex_user_reset-password.md @@ -30,10 +30,10 @@ stackit mongodbflex user reset-password USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_mongodbflex_user_update.md b/docs/stackit_mongodbflex_user_update.md index 31a674972..f97939851 100644 --- a/docs/stackit_mongodbflex_user_update.md +++ b/docs/stackit_mongodbflex_user_update.md @@ -23,7 +23,7 @@ stackit mongodbflex user update USER_ID [flags] --database string The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it -h, --help Help for "stackit mongodbflex user update" --instance-id string ID of the instance - --role strings Roles of the user, possible values are ["read" "readWrite" "readWriteAnyDatabase"] (default []) + --role strings Roles of the user. The "readAnyDatabase", "readWriteAnyDatabase" and "stackitAdmin" roles will always be created in the admin database. (multiple of: [read, readWrite, readAnyDatabase, readWriteAnyDatabase, stackitAdmin]) (default []) ``` ### Options inherited from parent commands @@ -31,10 +31,10 @@ stackit mongodbflex user update USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area.md b/docs/stackit_network-area.md index d9ba1ecda..19fa3091c 100644 --- a/docs/stackit_network-area.md +++ b/docs/stackit_network-area.md @@ -21,10 +21,10 @@ stackit network-area [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO @@ -35,6 +35,8 @@ stackit network-area [flags] * [stackit network-area describe](./stackit_network-area_describe.md) - Shows details of a STACKIT Network Area * [stackit network-area list](./stackit_network-area_list.md) - Lists all STACKIT Network Areas (SNA) of an organization * [stackit network-area network-range](./stackit_network-area_network-range.md) - Provides functionality for network ranges in STACKIT Network Areas +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) * [stackit network-area route](./stackit_network-area_route.md) - Provides functionality for static routes in STACKIT Network Areas +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes * [stackit network-area update](./stackit_network-area_update.md) - Updates a STACKIT Network Area (SNA) diff --git a/docs/stackit_network-area_create.md b/docs/stackit_network-area_create.md index 7dc278927..e9b29f2b8 100644 --- a/docs/stackit_network-area_create.md +++ b/docs/stackit_network-area_create.md @@ -13,32 +13,20 @@ stackit network-area create [flags] ### Examples ``` - Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network - $ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" + Create a network area with name "network-area-1" in organization with ID "xxx" + $ stackit network-area create --name network-area-1 --organization-id xxx" - Create a network area with name "network-area-2" in organization with ID "xxx" with network ranges, transfer network and DNS name server - $ stackit network-area create --name network-area-2 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --dns-name-servers "1.1.1.1" - - Create a network area with name "network-area-3" in organization with ID "xxx" with network ranges, transfer network and additional options - $ stackit network-area create --name network-area-3 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --default-prefix-length 25 --max-prefix-length 29 --min-prefix-length 24 - - Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network and labels "key=value,key1=value1" - $ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --labels key=value,key1=value1 + Create a network area with name "network-area-1" in organization with ID "xxx" with labels "key=value,key1=value1" + $ stackit network-area create --name network-area-1 --organization-id xxx --labels key=value,key1=value1 ``` ### Options ``` - --default-prefix-length int The default prefix length for networks in the network area - --dns-name-servers strings List of DNS name server IPs - -h, --help Help for "stackit network-area create" - --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) - --max-prefix-length int The maximum prefix length for networks in the network area - --min-prefix-length int The minimum prefix length for networks in the network area - -n, --name string Network area name - --network-ranges strings List of network ranges (default []) - --organization-id string Organization ID - --transfer-network string Transfer network in CIDR notation + -h, --help Help for "stackit network-area create" + --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) + -n, --name string Network area name + --organization-id string Organization ID ``` ### Options inherited from parent commands @@ -46,10 +34,10 @@ stackit network-area create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_delete.md b/docs/stackit_network-area_delete.md index f7814d583..d576cde46 100644 --- a/docs/stackit_network-area_delete.md +++ b/docs/stackit_network-area_delete.md @@ -31,10 +31,10 @@ stackit network-area delete AREA_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_describe.md b/docs/stackit_network-area_describe.md index a5656cfe7..e96d75699 100644 --- a/docs/stackit_network-area_describe.md +++ b/docs/stackit_network-area_describe.md @@ -36,10 +36,10 @@ stackit network-area describe AREA_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_list.md b/docs/stackit_network-area_list.md index 74fcaf9d7..24a7ee918 100644 --- a/docs/stackit_network-area_list.md +++ b/docs/stackit_network-area_list.md @@ -40,10 +40,10 @@ stackit network-area list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_network-range.md b/docs/stackit_network-area_network-range.md index 4e146f3d7..3f431fd9d 100644 --- a/docs/stackit_network-area_network-range.md +++ b/docs/stackit_network-area_network-range.md @@ -21,10 +21,10 @@ stackit network-area network-range [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_network-range_create.md b/docs/stackit_network-area_network-range_create.md index c51b7399a..a0b862957 100644 --- a/docs/stackit_network-area_network-range_create.md +++ b/docs/stackit_network-area_network-range_create.md @@ -31,10 +31,10 @@ stackit network-area network-range create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_network-range_delete.md b/docs/stackit_network-area_network-range_delete.md index 22626b6bf..f2838a2a2 100644 --- a/docs/stackit_network-area_network-range_delete.md +++ b/docs/stackit_network-area_network-range_delete.md @@ -30,10 +30,10 @@ stackit network-area network-range delete NETWORK_RANGE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_network-range_describe.md b/docs/stackit_network-area_network-range_describe.md index 5e1831f7f..112c407ba 100644 --- a/docs/stackit_network-area_network-range_describe.md +++ b/docs/stackit_network-area_network-range_describe.md @@ -30,10 +30,10 @@ stackit network-area network-range describe NETWORK_RANGE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_network-range_list.md b/docs/stackit_network-area_network-range_list.md index f66857ce7..d76b22ec4 100644 --- a/docs/stackit_network-area_network-range_list.md +++ b/docs/stackit_network-area_network-range_list.md @@ -37,10 +37,10 @@ stackit network-area network-range list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_region.md b/docs/stackit_network-area_region.md new file mode 100644 index 000000000..9db9465d7 --- /dev/null +++ b/docs/stackit_network-area_region.md @@ -0,0 +1,38 @@ +## stackit network-area region + +Provides functionality for regional configuration of STACKIT Network Area (SNA) + +### Synopsis + +Provides functionality for regional configuration of STACKIT Network Area (SNA). + +``` +stackit network-area region [flags] +``` + +### Options + +``` + -h, --help Help for "stackit network-area region" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area](./stackit_network-area.md) - Provides functionality for STACKIT Network Area (SNA) +* [stackit network-area region create](./stackit_network-area_region_create.md) - Creates a new regional configuration for a STACKIT Network Area (SNA) +* [stackit network-area region delete](./stackit_network-area_region_delete.md) - Deletes a regional configuration for a STACKIT Network Area (SNA) +* [stackit network-area region describe](./stackit_network-area_region_describe.md) - Describes a regional configuration for a STACKIT Network Area (SNA) +* [stackit network-area region list](./stackit_network-area_region_list.md) - Lists all configured regions for a STACKIT Network Area (SNA) +* [stackit network-area region update](./stackit_network-area_region_update.md) - Updates a existing regional configuration for a STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_create.md b/docs/stackit_network-area_region_create.md new file mode 100644 index 000000000..c787f189e --- /dev/null +++ b/docs/stackit_network-area_region_create.md @@ -0,0 +1,58 @@ +## stackit network-area region create + +Creates a new regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Creates a new regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region create [flags] +``` + +### Examples + +``` + Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24" and ipv4 transfer network "192.168.1.0/24" + $ stackit network-area region create --network-area-id xxx --region eu02 --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 + + Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 + + Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 + + Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 +``` + +### Options + +``` + -h, --help Help for "stackit network-area region create" + --ipv4-default-nameservers strings List of default DNS name server IPs + --ipv4-default-prefix-length int The default prefix length for networks in the network area + --ipv4-max-prefix-length int The maximum prefix length for networks in the network area + --ipv4-min-prefix-length int The minimum prefix length for networks in the network area + --ipv4-network-ranges strings Network range to create in CIDR notation (default []) + --ipv4-transfer-network string Transfer network in CIDR notation + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_delete.md b/docs/stackit_network-area_region_delete.md new file mode 100644 index 000000000..9a8d787aa --- /dev/null +++ b/docs/stackit_network-area_region_delete.md @@ -0,0 +1,46 @@ +## stackit network-area region delete + +Deletes a regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Deletes a regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region delete [flags] +``` + +### Examples + +``` + Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area region delete --network-area-id xxx --region eu02 --organization-id yyy + + Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region delete --network-area-id xxx --organization-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area region delete" + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_describe.md b/docs/stackit_network-area_region_describe.md new file mode 100644 index 000000000..6f1361d38 --- /dev/null +++ b/docs/stackit_network-area_region_describe.md @@ -0,0 +1,46 @@ +## stackit network-area region describe + +Describes a regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Describes a regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region describe [flags] +``` + +### Examples + +``` + Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area region describe --network-area-id xxx --region eu02 --organization-id yyy + + Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region describe --network-area-id xxx --organization-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area region describe" + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_list.md b/docs/stackit_network-area_region_list.md new file mode 100644 index 000000000..8c8ec725d --- /dev/null +++ b/docs/stackit_network-area_region_list.md @@ -0,0 +1,42 @@ +## stackit network-area region list + +Lists all configured regions for a STACKIT Network Area (SNA) + +### Synopsis + +Lists all configured regions for a STACKIT Network Area (SNA). + +``` +stackit network-area region list [flags] +``` + +### Examples + +``` + List all configured region for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area region list --network-area-id xxx --organization-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area region list" + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_region_update.md b/docs/stackit_network-area_region_update.md new file mode 100644 index 000000000..8e2429cc2 --- /dev/null +++ b/docs/stackit_network-area_region_update.md @@ -0,0 +1,56 @@ +## stackit network-area region update + +Updates a existing regional configuration for a STACKIT Network Area (SNA) + +### Synopsis + +Updates a existing regional configuration for a STACKIT Network Area (SNA). + +``` +stackit network-area region update [flags] +``` + +### Examples + +``` + Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8" + $ stackit network-area region update --network-area-id xxx --region eu02 --organization-id yyy --ipv4-default-nameservers 8.8.8.8 + + Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8", using the set region config + $ stackit config set --region eu02 + $ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-default-nameservers 8.8.8.8 + + Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 + + Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20" + $ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20 +``` + +### Options + +``` + -h, --help Help for "stackit network-area region update" + --ipv4-default-nameservers strings List of default DNS name server IPs + --ipv4-default-prefix-length int The default prefix length for networks in the network area + --ipv4-max-prefix-length int The maximum prefix length for networks in the network area + --ipv4-min-prefix-length int The minimum prefix length for networks in the network area + --network-area-id string STACKIT Network Area (SNA) ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area region](./stackit_network-area_region.md) - Provides functionality for regional configuration of STACKIT Network Area (SNA) + diff --git a/docs/stackit_network-area_route.md b/docs/stackit_network-area_route.md index a5fb3f19d..91d7964ae 100644 --- a/docs/stackit_network-area_route.md +++ b/docs/stackit_network-area_route.md @@ -21,10 +21,10 @@ stackit network-area route [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_route_create.md b/docs/stackit_network-area_route_create.md index 79d239fee..a6bba2074 100644 --- a/docs/stackit_network-area_route_create.md +++ b/docs/stackit_network-area_route_create.md @@ -15,22 +15,25 @@ stackit network-area route create [flags] ### Examples ``` - Create a static route with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" - $ stackit network-area route create --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1 + Create a static route with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area route create --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1 - Create a static route with labels "key:value" and "foo:bar" with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" - $ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1 + Create a static route with labels "key:value" and "foo:bar" with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy" + $ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1 ``` ### Options ``` + --destination string Destination route. Must be a valid IPv4 or IPv6 CIDR -h, --help Help for "stackit network-area route create" --labels stringToString Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default []) --network-area-id string STACKIT Network Area ID - --next-hop string Next hop IP address. Must be a valid IPv4 + --next-hop-ipv4 string Next hop IPv4 address + --next-hop-ipv6 string Next hop IPv6 address + --nexthop-blackhole Sets next hop to black hole + --nexthop-internet Sets next hop to internet --organization-id string Organization ID - --prefix string Static route prefix ``` ### Options inherited from parent commands @@ -38,10 +41,10 @@ stackit network-area route create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_route_delete.md b/docs/stackit_network-area_route_delete.md index fc95549b4..14332b576 100644 --- a/docs/stackit_network-area_route_delete.md +++ b/docs/stackit_network-area_route_delete.md @@ -30,10 +30,10 @@ stackit network-area route delete ROUTE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_route_describe.md b/docs/stackit_network-area_route_describe.md index fbacf05bd..0440767ea 100644 --- a/docs/stackit_network-area_route_describe.md +++ b/docs/stackit_network-area_route_describe.md @@ -33,10 +33,10 @@ stackit network-area route describe ROUTE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_route_list.md b/docs/stackit_network-area_route_list.md index ff0a6ab1e..72b1b6a98 100644 --- a/docs/stackit_network-area_route_list.md +++ b/docs/stackit_network-area_route_list.md @@ -37,10 +37,10 @@ stackit network-area route list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_route_update.md b/docs/stackit_network-area_route_update.md index 61e54d10f..b28d89d38 100644 --- a/docs/stackit_network-area_route_update.md +++ b/docs/stackit_network-area_route_update.md @@ -33,10 +33,10 @@ stackit network-area route update ROUTE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-area_routing-table.md b/docs/stackit_network-area_routing-table.md new file mode 100644 index 000000000..b39b4468e --- /dev/null +++ b/docs/stackit_network-area_routing-table.md @@ -0,0 +1,42 @@ +## stackit network-area routing-table + +Manage routing-tables and its according routes + +### Synopsis + +Manage routing-tables and their associated routes. + +This API is currently available only to selected customers. +To request access, please contact your account manager or submit a support ticket. + +``` +stackit network-area routing-table [flags] +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area](./stackit_network-area.md) - Provides functionality for STACKIT Network Area (SNA) +* [stackit network-area routing-table create](./stackit_network-area_routing-table_create.md) - Creates a routing-table +* [stackit network-area routing-table delete](./stackit_network-area_routing-table_delete.md) - Deletes a routing-table +* [stackit network-area routing-table describe](./stackit_network-area_routing-table_describe.md) - Describes a routing-table +* [stackit network-area routing-table list](./stackit_network-area_routing-table_list.md) - Lists all routing-tables +* [stackit network-area routing-table route](./stackit_network-area_routing-table_route.md) - Manages routes of a routing-table +* [stackit network-area routing-table update](./stackit_network-area_routing-table_update.md) - Updates a routing-table + diff --git a/docs/stackit_network-area_routing-table_create.md b/docs/stackit_network-area_routing-table_create.md new file mode 100644 index 000000000..e4af0401d --- /dev/null +++ b/docs/stackit_network-area_routing-table_create.md @@ -0,0 +1,56 @@ +## stackit network-area routing-table create + +Creates a routing-table + +### Synopsis + +Creates a routing-table. + +``` +stackit network-area routing-table create [flags] +``` + +### Examples + +``` + Create a routing-table with name "rt" + $ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" + + Create a routing-table with name "rt" and description "some description" + $ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" --description "some description" + + Create a routing-table with name "rt" with system routes disabled + $ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" --system-routes=false + + Create a routing-table with name "rt" with dynamic routes disabled + $ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" --dynamic-routes=false +``` + +### Options + +``` + --description string Description of the routing-table + --dynamic-routes If set to false, prevents dynamic routes from propagating to the routing table. (default true) + -h, --help Help for "stackit network-area routing-table create" + --labels stringToString Key=value labels (default []) + --name string Name of the routing-table + --network-area-id string Network-Area ID + --organization-id string Organization ID + --system-routes If set to false, disables routes for project-to-project communication. (default true) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_network-area_routing-table_delete.md b/docs/stackit_network-area_routing-table_delete.md new file mode 100644 index 000000000..71225b8fd --- /dev/null +++ b/docs/stackit_network-area_routing-table_delete.md @@ -0,0 +1,42 @@ +## stackit network-area routing-table delete + +Deletes a routing-table + +### Synopsis + +Deletes a routing-table + +``` +stackit network-area routing-table delete ROUTING_TABLE_ID [flags] +``` + +### Examples + +``` + Delete a routing-table with ID "xxx" + $ stackit network-area routing-table delete xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table delete" + --network-area-id string Network-Area ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_network-area_routing-table_describe.md b/docs/stackit_network-area_routing-table_describe.md new file mode 100644 index 000000000..cce4804c4 --- /dev/null +++ b/docs/stackit_network-area_routing-table_describe.md @@ -0,0 +1,42 @@ +## stackit network-area routing-table describe + +Describes a routing-table + +### Synopsis + +Describes a routing-table + +``` +stackit network-area routing-table describe ROUTING_TABLE_ID [flags] +``` + +### Examples + +``` + Describe a routing-table + $ stackit network-area routing-table describe xxx --organization-id xxx --network-area-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table describe" + --network-area-id string Network-Area ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_network-area_routing-table_list.md b/docs/stackit_network-area_routing-table_list.md new file mode 100644 index 000000000..63660ca06 --- /dev/null +++ b/docs/stackit_network-area_routing-table_list.md @@ -0,0 +1,50 @@ +## stackit network-area routing-table list + +Lists all routing-tables + +### Synopsis + +Lists all routing-tables + +``` +stackit network-area routing-table list [flags] +``` + +### Examples + +``` + List all routing-tables + $ stackit network-area routing-table list --organization-id xxx --network-area-id yyy + + List all routing-tables with labels + $ stackit network-area routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy + + List all routing-tables with labels and set limit to 10 + $ stackit network-area routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table list" + --label-selector string Filter by label + --limit int Maximum number of entries to list + --network-area-id string Network-Area ID + --organization-id string Organization ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_network-area_routing-table_route.md b/docs/stackit_network-area_routing-table_route.md new file mode 100644 index 000000000..8424f8203 --- /dev/null +++ b/docs/stackit_network-area_routing-table_route.md @@ -0,0 +1,38 @@ +## stackit network-area routing-table route + +Manages routes of a routing-table + +### Synopsis + +Manages routes of a routing-table + +``` +stackit network-area routing-table route [flags] +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table route" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes +* [stackit network-area routing-table route create](./stackit_network-area_routing-table_route_create.md) - Creates a route in a routing-table +* [stackit network-area routing-table route delete](./stackit_network-area_routing-table_route_delete.md) - Deletes a route within a routing-table +* [stackit network-area routing-table route describe](./stackit_network-area_routing-table_route_describe.md) - Describes a route within a routing-table +* [stackit network-area routing-table route list](./stackit_network-area_routing-table_route_list.md) - Lists all routes within a routing-table +* [stackit network-area routing-table route update](./stackit_network-area_routing-table_route_update.md) - Updates a route in a routing-table + diff --git a/docs/stackit_network-area_routing-table_route_create.md b/docs/stackit_network-area_routing-table_route_create.md new file mode 100644 index 000000000..80984448a --- /dev/null +++ b/docs/stackit_network-area_routing-table_route_create.md @@ -0,0 +1,54 @@ +## stackit network-area routing-table route create + +Creates a route in a routing-table + +### Synopsis + +Creates a route in a routing-table. + +``` +stackit network-area routing-table route create [flags] +``` + +### Examples + +``` + Create a route with CIDRv4 destination and IPv4 nexthop + $ stackit network-area routing-table route create --routing-table-id xxx --organization-id yyy --network-area-id zzz --destination-type cidrv4 --destination-value --nexthop-type ipv4 --nexthop-value + + Create a route with CIDRv6 destination and IPv6 nexthop + $ stackit network-area routing-table route create --routing-table-id xxx --organization-id yyy --network-area-id zzz --destination-type cidrv6 --destination-value --nexthop-type ipv6 --nexthop-value + + Create a route with CIDRv6 destination and Nexthop Internet + $ stackit network-area routing-table route create --routing-table-id xxx --organization-id yyy --network-area-id zzz --destination-type cidrv6 --destination-value --nexthop-type internet +``` + +### Options + +``` + --destination-type string Destination type (one of: [cidrv4, cidrv6]) + --destination-value string Destination value + -h, --help Help for "stackit network-area routing-table route create" + --labels stringToString Key=value labels (default []) + --network-area-id string Network-Area ID + --nexthop-type string Next hop type (one of: [ipv4, ipv6, internet, blackhole]) + --nexthop-value string NextHop value + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table route](./stackit_network-area_routing-table_route.md) - Manages routes of a routing-table + diff --git a/docs/stackit_network-area_routing-table_route_delete.md b/docs/stackit_network-area_routing-table_route_delete.md new file mode 100644 index 000000000..80f5dcbd0 --- /dev/null +++ b/docs/stackit_network-area_routing-table_route_delete.md @@ -0,0 +1,43 @@ +## stackit network-area routing-table route delete + +Deletes a route within a routing-table + +### Synopsis + +Deletes a route within a routing-table + +``` +stackit network-area routing-table route delete routing-table-id [flags] +``` + +### Examples + +``` + Deletes a route within a routing-table + $ stackit network-area routing-table route delete xxx --routing-table-id xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table route delete" + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table route](./stackit_network-area_routing-table_route.md) - Manages routes of a routing-table + diff --git a/docs/stackit_network-area_routing-table_route_describe.md b/docs/stackit_network-area_routing-table_route_describe.md new file mode 100644 index 000000000..7eec6bd57 --- /dev/null +++ b/docs/stackit_network-area_routing-table_route_describe.md @@ -0,0 +1,43 @@ +## stackit network-area routing-table route describe + +Describes a route within a routing-table + +### Synopsis + +Describes a route within a routing-table + +``` +stackit network-area routing-table route describe ROUTE_ID [flags] +``` + +### Examples + +``` + Describe a route within a routing-table + $ stackit network-area routing-table route describe xxx --routing-table-id xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table route describe" + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table route](./stackit_network-area_routing-table_route.md) - Manages routes of a routing-table + diff --git a/docs/stackit_network-area_routing-table_route_list.md b/docs/stackit_network-area_routing-table_route_list.md new file mode 100644 index 000000000..1cfde45e7 --- /dev/null +++ b/docs/stackit_network-area_routing-table_route_list.md @@ -0,0 +1,51 @@ +## stackit network-area routing-table route list + +Lists all routes within a routing-table + +### Synopsis + +Lists all routes within a routing-table + +``` +stackit network-area routing-table route list [flags] +``` + +### Examples + +``` + List all routes within a routing-table + $ stackit network-area routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz + + List all routes within a routing-table with labels + $ stackit network-area routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc + + List all routes within a routing-tables with labels and limit to 10 + $ stackit network-area routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table route list" + --label-selector string Filter by label + --limit int Maximum number of entries to list + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table route](./stackit_network-area_routing-table_route.md) - Manages routes of a routing-table + diff --git a/docs/stackit_network-area_routing-table_route_update.md b/docs/stackit_network-area_routing-table_route_update.md new file mode 100644 index 000000000..aa6daec2b --- /dev/null +++ b/docs/stackit_network-area_routing-table_route_update.md @@ -0,0 +1,44 @@ +## stackit network-area routing-table route update + +Updates a route in a routing-table + +### Synopsis + +Updates a route in a routing-table. + +``` +stackit network-area routing-table route update ROUTE_ID [flags] +``` + +### Examples + +``` + Updates the label(s) of a route with ID "xxx" in a routing-table ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit network-area routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz +``` + +### Options + +``` + -h, --help Help for "stackit network-area routing-table route update" + --labels stringToString Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels (default []) + --network-area-id string Network-Area ID + --organization-id string Organization ID + --routing-table-id string Routing-Table ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table route](./stackit_network-area_routing-table_route.md) - Manages routes of a routing-table + diff --git a/docs/stackit_network-area_routing-table_update.md b/docs/stackit_network-area_routing-table_update.md new file mode 100644 index 000000000..b6803179a --- /dev/null +++ b/docs/stackit_network-area_routing-table_update.md @@ -0,0 +1,59 @@ +## stackit network-area routing-table update + +Updates a routing-table + +### Synopsis + +Updates a routing-table. + +``` +stackit network-area routing-table update ROUTING_TABLE_ID [flags] +``` + +### Examples + +``` + Updates the label(s) of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit network-area routing-table update xxx --labels key=value,foo=bar --organization-id yyy --network-area-id zzz + + Updates the name of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit network-area routing-table update xxx --name foo --organization-id yyy --network-area-id zzz + + Updates the description of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit network-area routing-table update xxx --description foo --organization-id yyy --network-area-id zzz + + Disables the dynamic routes of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit network-area routing-table update xxx --organization-id yyy --network-area-id zzz --dynamic-routes=false + + Disables the system routes of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz" + $ stackit network-area routing-table update xxx --organization-id yyy --network-area-id zzz --system-routes=false +``` + +### Options + +``` + --description string Description of the routing-table + --dynamic-routes If set to false, prevents dynamic routes from propagating to the routing table. + -h, --help Help for "stackit network-area routing-table update" + --labels stringToString Key=value labels (default []) + --name string Name of the routing-table + --network-area-id string Network-Area ID + --organization-id string Organization ID + --system-routes If set to false, disables routes for project-to-project communication. +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit network-area routing-table](./stackit_network-area_routing-table.md) - Manage routing-tables and its according routes + diff --git a/docs/stackit_network-area_update.md b/docs/stackit_network-area_update.md index 57b32a662..b9d6cd546 100644 --- a/docs/stackit_network-area_update.md +++ b/docs/stackit_network-area_update.md @@ -20,14 +20,10 @@ stackit network-area update AREA_ID [flags] ### Options ``` - --default-prefix-length int The default prefix length for networks in the network area - --dns-name-servers strings List of DNS name server IPs - -h, --help Help for "stackit network-area update" - --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) - --max-prefix-length int The maximum prefix length for networks in the network area - --min-prefix-length int The minimum prefix length for networks in the network area - -n, --name string Network area name - --organization-id string Organization ID + -h, --help Help for "stackit network-area update" + --labels stringToString Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...' (default []) + -n, --name string Network area name + --organization-id string Organization ID ``` ### Options inherited from parent commands @@ -35,10 +31,10 @@ stackit network-area update AREA_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-interface.md b/docs/stackit_network-interface.md index b7be67c0c..1b022b19e 100644 --- a/docs/stackit_network-interface.md +++ b/docs/stackit_network-interface.md @@ -21,10 +21,10 @@ stackit network-interface [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-interface_create.md b/docs/stackit_network-interface_create.md index 5ff3acd27..b42934899 100644 --- a/docs/stackit_network-interface_create.md +++ b/docs/stackit_network-interface_create.md @@ -39,10 +39,10 @@ stackit network-interface create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-interface_delete.md b/docs/stackit_network-interface_delete.md index 624e0b83f..3d40859e7 100644 --- a/docs/stackit_network-interface_delete.md +++ b/docs/stackit_network-interface_delete.md @@ -29,10 +29,10 @@ stackit network-interface delete NIC_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-interface_describe.md b/docs/stackit_network-interface_describe.md index 159475be8..60aea3df6 100644 --- a/docs/stackit_network-interface_describe.md +++ b/docs/stackit_network-interface_describe.md @@ -35,10 +35,10 @@ stackit network-interface describe NIC_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-interface_list.md b/docs/stackit_network-interface_list.md index f202a6667..acfdcb6fb 100644 --- a/docs/stackit_network-interface_list.md +++ b/docs/stackit_network-interface_list.md @@ -13,6 +13,9 @@ stackit network-interface list [flags] ### Examples ``` + Lists all network interfaces + $ stackit network-interface list + Lists all network interfaces with network ID "xxx" $ stackit network-interface list --network-id xxx @@ -40,10 +43,10 @@ stackit network-interface list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network-interface_update.md b/docs/stackit_network-interface_update.md index 0c3e2c322..54e9e2334 100644 --- a/docs/stackit_network-interface_update.md +++ b/docs/stackit_network-interface_update.md @@ -40,10 +40,10 @@ stackit network-interface update NIC_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network.md b/docs/stackit_network.md index f196fe7b6..b443e615a 100644 --- a/docs/stackit_network.md +++ b/docs/stackit_network.md @@ -21,10 +21,10 @@ stackit network [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network_create.md b/docs/stackit_network_create.md index 21d9e863c..9d6ed3097 100644 --- a/docs/stackit_network_create.md +++ b/docs/stackit_network_create.md @@ -26,10 +26,13 @@ stackit network create [flags] $ stackit network create --name network-1 --labels key=value,key1=value1 Create an IPv4 network with name "network-1" with DNS name servers, a prefix and a gateway - $ stackit network create --name network-1 --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" --ipv4-gateway "10.1.2.3" + $ stackit network create --name network-1 --non-routed --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" --ipv4-gateway "10.1.2.3" Create an IPv6 network with name "network-1" with DNS name servers, a prefix and a gateway $ stackit network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888" + + Create a network with name "network-1" and attach routing-table "xxx" + $ stackit network create --name network-1 --routing-table-id xxx ``` ### Options @@ -49,6 +52,7 @@ stackit network create [flags] --no-ipv4-gateway If set to true, the network doesn't have an IPv4 gateway --no-ipv6-gateway If set to true, the network doesn't have an IPv6 gateway --non-routed If set to true, the network is not routed and therefore not accessible from other networks + --routing-table-id string The ID of the routing-table for the network ``` ### Options inherited from parent commands @@ -56,10 +60,10 @@ stackit network create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network_delete.md b/docs/stackit_network_delete.md index 5fb62e6e3..2765ce106 100644 --- a/docs/stackit_network_delete.md +++ b/docs/stackit_network_delete.md @@ -30,10 +30,10 @@ stackit network delete NETWORK_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network_describe.md b/docs/stackit_network_describe.md index d7298e4e5..e432ea4e5 100644 --- a/docs/stackit_network_describe.md +++ b/docs/stackit_network_describe.md @@ -31,10 +31,10 @@ stackit network describe NETWORK_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network_list.md b/docs/stackit_network_list.md index 1b4febd39..aedb0c8bc 100644 --- a/docs/stackit_network_list.md +++ b/docs/stackit_network_list.md @@ -39,10 +39,10 @@ stackit network list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_network_update.md b/docs/stackit_network_update.md index 313ce68fa..fbd5523de 100644 --- a/docs/stackit_network_update.md +++ b/docs/stackit_network_update.md @@ -24,6 +24,9 @@ stackit network update NETWORK_ID [flags] Update IPv6 network with ID "xxx" with new name "network-1-new", new gateway and new DNS name servers $ stackit network update xxx --name network-1-new --ipv6-dns-name-servers "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888" + + Update network with ID "xxx" with new routing-table id "xxx" + $ stackit network update xxx --routing-table-id xxx ``` ### Options @@ -38,6 +41,7 @@ stackit network update NETWORK_ID [flags] -n, --name string Network name --no-ipv4-gateway If set to true, the network doesn't have an IPv4 gateway --no-ipv6-gateway If set to true, the network doesn't have an IPv6 gateway + --routing-table-id string The ID of the routing-table for the network ``` ### Options inherited from parent commands @@ -45,10 +49,10 @@ stackit network update NETWORK_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage.md b/docs/stackit_object-storage.md index 5caa02380..bce1eae75 100644 --- a/docs/stackit_object-storage.md +++ b/docs/stackit_object-storage.md @@ -21,16 +21,17 @@ stackit object-storage [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit object-storage bucket](./stackit_object-storage_bucket.md) - Provides functionality for Object Storage buckets +* [stackit object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock * [stackit object-storage credentials](./stackit_object-storage_credentials.md) - Provides functionality for Object Storage credentials * [stackit object-storage credentials-group](./stackit_object-storage_credentials-group.md) - Provides functionality for Object Storage credentials group * [stackit object-storage disable](./stackit_object-storage_disable.md) - Disables Object Storage for a project diff --git a/docs/stackit_object-storage_bucket.md b/docs/stackit_object-storage_bucket.md index ac5d3b600..40fa6bf81 100644 --- a/docs/stackit_object-storage_bucket.md +++ b/docs/stackit_object-storage_bucket.md @@ -21,10 +21,10 @@ stackit object-storage bucket [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_bucket_create.md b/docs/stackit_object-storage_bucket_create.md index 1c07eb540..e4a420de6 100644 --- a/docs/stackit_object-storage_bucket_create.md +++ b/docs/stackit_object-storage_bucket_create.md @@ -15,12 +15,16 @@ stackit object-storage bucket create BUCKET_NAME [flags] ``` Create an Object Storage bucket with name "my-bucket" $ stackit object-storage bucket create my-bucket + + Create an Object Storage bucket with enabled object-lock + $ stackit object-storage bucket create my-bucket --object-lock-enabled ``` ### Options ``` - -h, --help Help for "stackit object-storage bucket create" + -h, --help Help for "stackit object-storage bucket create" + --object-lock-enabled is the object-lock enabled for the bucket ``` ### Options inherited from parent commands @@ -28,10 +32,10 @@ stackit object-storage bucket create BUCKET_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_bucket_delete.md b/docs/stackit_object-storage_bucket_delete.md index d512e4625..74cf3436e 100644 --- a/docs/stackit_object-storage_bucket_delete.md +++ b/docs/stackit_object-storage_bucket_delete.md @@ -28,10 +28,10 @@ stackit object-storage bucket delete BUCKET_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_bucket_describe.md b/docs/stackit_object-storage_bucket_describe.md index 256269aa9..adebdd92e 100644 --- a/docs/stackit_object-storage_bucket_describe.md +++ b/docs/stackit_object-storage_bucket_describe.md @@ -31,10 +31,10 @@ stackit object-storage bucket describe BUCKET_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_bucket_list.md b/docs/stackit_object-storage_bucket_list.md index b77f0eecb..6473abb83 100644 --- a/docs/stackit_object-storage_bucket_list.md +++ b/docs/stackit_object-storage_bucket_list.md @@ -35,10 +35,10 @@ stackit object-storage bucket list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_compliance-lock.md b/docs/stackit_object-storage_compliance-lock.md new file mode 100644 index 000000000..e25cf4da0 --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock.md @@ -0,0 +1,36 @@ +## stackit object-storage compliance-lock + +Provides functionality to manage Object Storage compliance lock + +### Synopsis + +Provides functionality to manage Object Storage compliance lock. + +``` +stackit object-storage compliance-lock [flags] +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit object-storage](./stackit_object-storage.md) - Provides functionality for Object Storage +* [stackit object-storage compliance-lock describe](./stackit_object-storage_compliance-lock_describe.md) - Describe object storage compliance lock +* [stackit object-storage compliance-lock lock](./stackit_object-storage_compliance-lock_lock.md) - Create object storage compliance lock +* [stackit object-storage compliance-lock unlock](./stackit_object-storage_compliance-lock_unlock.md) - Delete object storage compliance lock + diff --git a/docs/stackit_object-storage_compliance-lock_describe.md b/docs/stackit_object-storage_compliance-lock_describe.md new file mode 100644 index 000000000..c88bdfac8 --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock_describe.md @@ -0,0 +1,40 @@ +## stackit object-storage compliance-lock describe + +Describe object storage compliance lock + +### Synopsis + +Describe object storage compliance lock. + +``` +stackit object-storage compliance-lock describe [flags] +``` + +### Examples + +``` + Describe object storage compliance lock + $ stackit object-storage compliance-lock describe +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock + diff --git a/docs/stackit_object-storage_compliance-lock_lock.md b/docs/stackit_object-storage_compliance-lock_lock.md new file mode 100644 index 000000000..db2ae51bb --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock_lock.md @@ -0,0 +1,40 @@ +## stackit object-storage compliance-lock lock + +Create object storage compliance lock + +### Synopsis + +Create object storage compliance lock. + +``` +stackit object-storage compliance-lock lock [flags] +``` + +### Examples + +``` + Create object storage compliance lock + $ stackit object-storage compliance-lock lock +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock lock" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock + diff --git a/docs/stackit_object-storage_compliance-lock_unlock.md b/docs/stackit_object-storage_compliance-lock_unlock.md new file mode 100644 index 000000000..4d0d552a7 --- /dev/null +++ b/docs/stackit_object-storage_compliance-lock_unlock.md @@ -0,0 +1,40 @@ +## stackit object-storage compliance-lock unlock + +Delete object storage compliance lock + +### Synopsis + +Delete object storage compliance lock. + +``` +stackit object-storage compliance-lock unlock [flags] +``` + +### Examples + +``` + Delete object storage compliance lock + $ stackit object-storage compliance-lock unlock +``` + +### Options + +``` + -h, --help Help for "stackit object-storage compliance-lock unlock" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit object-storage compliance-lock](./stackit_object-storage_compliance-lock.md) - Provides functionality to manage Object Storage compliance lock + diff --git a/docs/stackit_object-storage_credentials-group.md b/docs/stackit_object-storage_credentials-group.md index d20daee64..7d8d5a18a 100644 --- a/docs/stackit_object-storage_credentials-group.md +++ b/docs/stackit_object-storage_credentials-group.md @@ -21,10 +21,10 @@ stackit object-storage credentials-group [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials-group_create.md b/docs/stackit_object-storage_credentials-group_create.md index 2dd99a88e..b32060e33 100644 --- a/docs/stackit_object-storage_credentials-group_create.md +++ b/docs/stackit_object-storage_credentials-group_create.md @@ -29,10 +29,10 @@ stackit object-storage credentials-group create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials-group_delete.md b/docs/stackit_object-storage_credentials-group_delete.md index 4d97f4722..15d49240a 100644 --- a/docs/stackit_object-storage_credentials-group_delete.md +++ b/docs/stackit_object-storage_credentials-group_delete.md @@ -28,10 +28,10 @@ stackit object-storage credentials-group delete CREDENTIALS_GROUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials-group_list.md b/docs/stackit_object-storage_credentials-group_list.md index 2cb925069..28c09705b 100644 --- a/docs/stackit_object-storage_credentials-group_list.md +++ b/docs/stackit_object-storage_credentials-group_list.md @@ -35,10 +35,10 @@ stackit object-storage credentials-group list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials.md b/docs/stackit_object-storage_credentials.md index 2427b5f42..a1f77badf 100644 --- a/docs/stackit_object-storage_credentials.md +++ b/docs/stackit_object-storage_credentials.md @@ -21,10 +21,10 @@ stackit object-storage credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials_create.md b/docs/stackit_object-storage_credentials_create.md index 95df0d984..c88b42f28 100644 --- a/docs/stackit_object-storage_credentials_create.md +++ b/docs/stackit_object-storage_credentials_create.md @@ -33,10 +33,10 @@ stackit object-storage credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials_delete.md b/docs/stackit_object-storage_credentials_delete.md index 91768a44e..d82cb99ee 100644 --- a/docs/stackit_object-storage_credentials_delete.md +++ b/docs/stackit_object-storage_credentials_delete.md @@ -29,10 +29,10 @@ stackit object-storage credentials delete CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_credentials_list.md b/docs/stackit_object-storage_credentials_list.md index 08c2e94c2..f39d3a7b9 100644 --- a/docs/stackit_object-storage_credentials_list.md +++ b/docs/stackit_object-storage_credentials_list.md @@ -36,10 +36,10 @@ stackit object-storage credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_disable.md b/docs/stackit_object-storage_disable.md index c9aef3194..7f355efb2 100644 --- a/docs/stackit_object-storage_disable.md +++ b/docs/stackit_object-storage_disable.md @@ -28,10 +28,10 @@ stackit object-storage disable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_object-storage_enable.md b/docs/stackit_object-storage_enable.md index 9de05bfb3..aa23e9d89 100644 --- a/docs/stackit_object-storage_enable.md +++ b/docs/stackit_object-storage_enable.md @@ -28,10 +28,10 @@ stackit object-storage enable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability.md b/docs/stackit_observability.md index 393f10ff0..e8f0912de 100644 --- a/docs/stackit_observability.md +++ b/docs/stackit_observability.md @@ -21,10 +21,10 @@ stackit observability [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_credentials.md b/docs/stackit_observability_credentials.md index 661db4f24..7b6e155a7 100644 --- a/docs/stackit_observability_credentials.md +++ b/docs/stackit_observability_credentials.md @@ -21,10 +21,10 @@ stackit observability credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_credentials_create.md b/docs/stackit_observability_credentials_create.md index e850b8418..062a85746 100644 --- a/docs/stackit_observability_credentials_create.md +++ b/docs/stackit_observability_credentials_create.md @@ -30,10 +30,10 @@ stackit observability credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_credentials_delete.md b/docs/stackit_observability_credentials_delete.md index 98b927431..062062417 100644 --- a/docs/stackit_observability_credentials_delete.md +++ b/docs/stackit_observability_credentials_delete.md @@ -29,10 +29,10 @@ stackit observability credentials delete USERNAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_credentials_list.md b/docs/stackit_observability_credentials_list.md index 905f6658d..45fbff36f 100644 --- a/docs/stackit_observability_credentials_list.md +++ b/docs/stackit_observability_credentials_list.md @@ -36,10 +36,10 @@ stackit observability credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana.md b/docs/stackit_observability_grafana.md index 57f7a6440..67371e66f 100644 --- a/docs/stackit_observability_grafana.md +++ b/docs/stackit_observability_grafana.md @@ -21,10 +21,10 @@ stackit observability grafana [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_describe.md b/docs/stackit_observability_grafana_describe.md index 4eea4982a..6cbb5e2c4 100644 --- a/docs/stackit_observability_grafana_describe.md +++ b/docs/stackit_observability_grafana_describe.md @@ -6,7 +6,6 @@ Shows details of the Grafana configuration of an Observability instance Shows details of the Grafana configuration of an Observability instance. The Grafana dashboard URL and initial credentials (admin user and password) will be shown in the "pretty" output format. These credentials are only valid for first login. Please change the password after first login. After changing, the initial password is no longer valid. -The initial password is hidden by default, if you want to show it use the "--show-password" flag. ``` stackit observability grafana describe INSTANCE_ID [flags] @@ -18,9 +17,6 @@ stackit observability grafana describe INSTANCE_ID [flags] Get details of the Grafana configuration of an Observability instance with ID "xxx" $ stackit observability grafana describe xxx - Get details of the Grafana configuration of an Observability instance with ID "xxx" and show the initial admin password - $ stackit observability grafana describe xxx --show-password - Get details of the Grafana configuration of an Observability instance with ID "xxx" in JSON format $ stackit observability grafana describe xxx --output-format json ``` @@ -28,8 +24,7 @@ stackit observability grafana describe INSTANCE_ID [flags] ### Options ``` - -h, --help Help for "stackit observability grafana describe" - -s, --show-password Show password in output + -h, --help Help for "stackit observability grafana describe" ``` ### Options inherited from parent commands @@ -37,10 +32,10 @@ stackit observability grafana describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_public-read-access.md b/docs/stackit_observability_grafana_public-read-access.md index 39db9ef60..56d9fbc8f 100644 --- a/docs/stackit_observability_grafana_public-read-access.md +++ b/docs/stackit_observability_grafana_public-read-access.md @@ -22,10 +22,10 @@ stackit observability grafana public-read-access [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_public-read-access_disable.md b/docs/stackit_observability_grafana_public-read-access_disable.md index 02566e9f8..d62db72ac 100644 --- a/docs/stackit_observability_grafana_public-read-access_disable.md +++ b/docs/stackit_observability_grafana_public-read-access_disable.md @@ -29,10 +29,10 @@ stackit observability grafana public-read-access disable INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_public-read-access_enable.md b/docs/stackit_observability_grafana_public-read-access_enable.md index 7b71bbb4e..82883deda 100644 --- a/docs/stackit_observability_grafana_public-read-access_enable.md +++ b/docs/stackit_observability_grafana_public-read-access_enable.md @@ -29,10 +29,10 @@ stackit observability grafana public-read-access enable INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_single-sign-on.md b/docs/stackit_observability_grafana_single-sign-on.md index dfba3ff2e..bd9273bd9 100644 --- a/docs/stackit_observability_grafana_single-sign-on.md +++ b/docs/stackit_observability_grafana_single-sign-on.md @@ -22,10 +22,10 @@ stackit observability grafana single-sign-on [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_single-sign-on_disable.md b/docs/stackit_observability_grafana_single-sign-on_disable.md index e4907acfa..39ded4216 100644 --- a/docs/stackit_observability_grafana_single-sign-on_disable.md +++ b/docs/stackit_observability_grafana_single-sign-on_disable.md @@ -29,10 +29,10 @@ stackit observability grafana single-sign-on disable INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_grafana_single-sign-on_enable.md b/docs/stackit_observability_grafana_single-sign-on_enable.md index f083b1536..96c936aa1 100644 --- a/docs/stackit_observability_grafana_single-sign-on_enable.md +++ b/docs/stackit_observability_grafana_single-sign-on_enable.md @@ -29,10 +29,10 @@ stackit observability grafana single-sign-on enable INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_instance.md b/docs/stackit_observability_instance.md index 4b7350a5f..4b000ecb9 100644 --- a/docs/stackit_observability_instance.md +++ b/docs/stackit_observability_instance.md @@ -21,10 +21,10 @@ stackit observability instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_instance_create.md b/docs/stackit_observability_instance_create.md index 994fda4ba..2b99ae565 100644 --- a/docs/stackit_observability_instance_create.md +++ b/docs/stackit_observability_instance_create.md @@ -34,10 +34,10 @@ stackit observability instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_instance_delete.md b/docs/stackit_observability_instance_delete.md index 30005abe4..d8b40f922 100644 --- a/docs/stackit_observability_instance_delete.md +++ b/docs/stackit_observability_instance_delete.md @@ -28,10 +28,10 @@ stackit observability instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_instance_describe.md b/docs/stackit_observability_instance_describe.md index d8f50173c..6a67f87d7 100644 --- a/docs/stackit_observability_instance_describe.md +++ b/docs/stackit_observability_instance_describe.md @@ -31,10 +31,10 @@ stackit observability instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_instance_list.md b/docs/stackit_observability_instance_list.md index 18062dfa0..cce0a5ee8 100644 --- a/docs/stackit_observability_instance_list.md +++ b/docs/stackit_observability_instance_list.md @@ -35,10 +35,10 @@ stackit observability instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_instance_update.md b/docs/stackit_observability_instance_update.md index fdbcc88f4..902334b8c 100644 --- a/docs/stackit_observability_instance_update.md +++ b/docs/stackit_observability_instance_update.md @@ -37,10 +37,10 @@ stackit observability instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_plans.md b/docs/stackit_observability_plans.md index c0fb3846d..982ff6728 100644 --- a/docs/stackit_observability_plans.md +++ b/docs/stackit_observability_plans.md @@ -35,10 +35,10 @@ stackit observability plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config.md b/docs/stackit_observability_scrape-config.md index cecb2058b..75f70c337 100644 --- a/docs/stackit_observability_scrape-config.md +++ b/docs/stackit_observability_scrape-config.md @@ -21,10 +21,10 @@ stackit observability scrape-config [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config_create.md b/docs/stackit_observability_scrape-config_create.md index f8db966bb..982dafa6e 100644 --- a/docs/stackit_observability_scrape-config_create.md +++ b/docs/stackit_observability_scrape-config_create.md @@ -44,10 +44,10 @@ stackit observability scrape-config create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config_delete.md b/docs/stackit_observability_scrape-config_delete.md index 1ea9678ee..22086f974 100644 --- a/docs/stackit_observability_scrape-config_delete.md +++ b/docs/stackit_observability_scrape-config_delete.md @@ -29,10 +29,10 @@ stackit observability scrape-config delete JOB_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config_describe.md b/docs/stackit_observability_scrape-config_describe.md index de6ad62cd..00c6bc335 100644 --- a/docs/stackit_observability_scrape-config_describe.md +++ b/docs/stackit_observability_scrape-config_describe.md @@ -32,10 +32,10 @@ stackit observability scrape-config describe JOB_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config_generate-payload.md b/docs/stackit_observability_scrape-config_generate-payload.md index 2cc4d0ad0..56a255be4 100644 --- a/docs/stackit_observability_scrape-config_generate-payload.md +++ b/docs/stackit_observability_scrape-config_generate-payload.md @@ -47,10 +47,10 @@ stackit observability scrape-config generate-payload [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config_list.md b/docs/stackit_observability_scrape-config_list.md index 0db93b027..2f06b3a90 100644 --- a/docs/stackit_observability_scrape-config_list.md +++ b/docs/stackit_observability_scrape-config_list.md @@ -36,10 +36,10 @@ stackit observability scrape-config list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_observability_scrape-config_update.md b/docs/stackit_observability_scrape-config_update.md index a92e5cfb5..674fb7f7a 100644 --- a/docs/stackit_observability_scrape-config_update.md +++ b/docs/stackit_observability_scrape-config_update.md @@ -40,10 +40,10 @@ stackit observability scrape-config update JOB_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch.md b/docs/stackit_opensearch.md index c83f878ba..cb38c4ccd 100644 --- a/docs/stackit_opensearch.md +++ b/docs/stackit_opensearch.md @@ -21,10 +21,10 @@ stackit opensearch [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_credentials.md b/docs/stackit_opensearch_credentials.md index 2af0661e5..cce6ea45c 100644 --- a/docs/stackit_opensearch_credentials.md +++ b/docs/stackit_opensearch_credentials.md @@ -21,10 +21,10 @@ stackit opensearch credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_credentials_create.md b/docs/stackit_opensearch_credentials_create.md index dce634d44..cfe32b2ac 100644 --- a/docs/stackit_opensearch_credentials_create.md +++ b/docs/stackit_opensearch_credentials_create.md @@ -33,10 +33,10 @@ stackit opensearch credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_credentials_delete.md b/docs/stackit_opensearch_credentials_delete.md index 34af83bbd..ebd5e996b 100644 --- a/docs/stackit_opensearch_credentials_delete.md +++ b/docs/stackit_opensearch_credentials_delete.md @@ -29,10 +29,10 @@ stackit opensearch credentials delete CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_credentials_describe.md b/docs/stackit_opensearch_credentials_describe.md index 5f7c93e61..a55f6ba57 100644 --- a/docs/stackit_opensearch_credentials_describe.md +++ b/docs/stackit_opensearch_credentials_describe.md @@ -32,10 +32,10 @@ stackit opensearch credentials describe CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_credentials_list.md b/docs/stackit_opensearch_credentials_list.md index 6cc855daa..7d67d4bc6 100644 --- a/docs/stackit_opensearch_credentials_list.md +++ b/docs/stackit_opensearch_credentials_list.md @@ -36,10 +36,10 @@ stackit opensearch credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_instance.md b/docs/stackit_opensearch_instance.md index 2b2df6283..4e42492e0 100644 --- a/docs/stackit_opensearch_instance.md +++ b/docs/stackit_opensearch_instance.md @@ -21,10 +21,10 @@ stackit opensearch instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_instance_create.md b/docs/stackit_opensearch_instance_create.md index 77b5aada1..8e3534d0a 100644 --- a/docs/stackit_opensearch_instance_create.md +++ b/docs/stackit_opensearch_instance_create.md @@ -30,13 +30,13 @@ stackit opensearch instance create [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit opensearch instance create" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID -n, --name string Instance name --plan-id string Plan ID --plan-name string Plan name - --plugin strings Plugin + --plugin strings Plugins (multiple of: [repository-s3, repository-azure, analysis-phonetic]) (default []) --syslog strings Syslog --version string Instance OpenSearch version ``` @@ -46,10 +46,10 @@ stackit opensearch instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_instance_delete.md b/docs/stackit_opensearch_instance_delete.md index 2783bc403..c40f7f12e 100644 --- a/docs/stackit_opensearch_instance_delete.md +++ b/docs/stackit_opensearch_instance_delete.md @@ -28,10 +28,10 @@ stackit opensearch instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_instance_describe.md b/docs/stackit_opensearch_instance_describe.md index 8246df4f3..f0b6e5a48 100644 --- a/docs/stackit_opensearch_instance_describe.md +++ b/docs/stackit_opensearch_instance_describe.md @@ -31,10 +31,10 @@ stackit opensearch instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_instance_list.md b/docs/stackit_opensearch_instance_list.md index 36904c646..1b9f644cc 100644 --- a/docs/stackit_opensearch_instance_list.md +++ b/docs/stackit_opensearch_instance_list.md @@ -35,10 +35,10 @@ stackit opensearch instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_instance_update.md b/docs/stackit_opensearch_instance_update.md index 8e174c120..508f46a05 100644 --- a/docs/stackit_opensearch_instance_update.md +++ b/docs/stackit_opensearch_instance_update.md @@ -27,12 +27,12 @@ stackit opensearch instance update INSTANCE_ID [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit opensearch instance update" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID --plan-id string Plan ID --plan-name string Plan name - --plugin strings Plugin + --plugin strings Plugins (multiple of: [repository-s3, repository-azure, analysis-phonetic]) (default []) --syslog strings Syslog --version string Instance OpenSearch version ``` @@ -42,10 +42,10 @@ stackit opensearch instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_opensearch_plans.md b/docs/stackit_opensearch_plans.md index 5617d7e2b..e9c521d45 100644 --- a/docs/stackit_opensearch_plans.md +++ b/docs/stackit_opensearch_plans.md @@ -35,10 +35,10 @@ stackit opensearch plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_organization.md b/docs/stackit_organization.md index f1bbaedde..9d2ad0965 100644 --- a/docs/stackit_organization.md +++ b/docs/stackit_organization.md @@ -22,15 +22,17 @@ stackit organization [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO * [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit organization describe](./stackit_organization_describe.md) - Show an organization +* [stackit organization list](./stackit_organization_list.md) - Lists all organizations * [stackit organization member](./stackit_organization_member.md) - Manages organization members * [stackit organization role](./stackit_organization_role.md) - Manages organization roles diff --git a/docs/stackit_organization_describe.md b/docs/stackit_organization_describe.md new file mode 100644 index 000000000..e927d03a7 --- /dev/null +++ b/docs/stackit_organization_describe.md @@ -0,0 +1,43 @@ +## stackit organization describe + +Show an organization + +### Synopsis + +Show an organization. + +``` +stackit organization describe [flags] +``` + +### Examples + +``` + Describe the organization with the organization uuid "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + $ stackit organization describe xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + Describe the organization with the container id "foo-bar-organization" + $ stackit organization describe foo-bar-organization +``` + +### Options + +``` + -h, --help Help for "stackit organization describe" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit organization](./stackit_organization.md) - Manages organizations + diff --git a/docs/stackit_organization_list.md b/docs/stackit_organization_list.md new file mode 100644 index 000000000..b04e969f4 --- /dev/null +++ b/docs/stackit_organization_list.md @@ -0,0 +1,44 @@ +## stackit organization list + +Lists all organizations + +### Synopsis + +Lists all organizations. + +``` +stackit organization list [flags] +``` + +### Examples + +``` + Lists organizations for your user + $ stackit organization list + + Lists the first 10 organizations + $ stackit organization list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit organization list" + --limit int Maximum number of entries to list (default 50) +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit organization](./stackit_organization.md) - Manages organizations + diff --git a/docs/stackit_organization_member.md b/docs/stackit_organization_member.md index 9a0c0233d..9a0a12a4b 100644 --- a/docs/stackit_organization_member.md +++ b/docs/stackit_organization_member.md @@ -21,10 +21,10 @@ stackit organization member [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_organization_member_add.md b/docs/stackit_organization_member_add.md index c3946d22c..cc3e9f97b 100644 --- a/docs/stackit_organization_member_add.md +++ b/docs/stackit_organization_member_add.md @@ -34,10 +34,10 @@ stackit organization member add SUBJECT [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_organization_member_list.md b/docs/stackit_organization_member_list.md index feb052304..209f402e7 100644 --- a/docs/stackit_organization_member_list.md +++ b/docs/stackit_organization_member_list.md @@ -29,7 +29,7 @@ stackit organization member list [flags] -h, --help Help for "stackit organization member list" --limit int Maximum number of entries to list --organization-id string The organization ID - --sort-by string Sort entries by a specific field, one of ["subject" "role"] (default "subject") + --sort-by string Sort entries by a specific field, (one of: [subject, role]) (default "subject") --subject string Filter by subject (Identifier of user, service account or client. Usually email address in case of users or name in case of clients) ``` @@ -38,10 +38,10 @@ stackit organization member list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_organization_member_remove.md b/docs/stackit_organization_member_remove.md index f1a876d67..4627dfbce 100644 --- a/docs/stackit_organization_member_remove.md +++ b/docs/stackit_organization_member_remove.md @@ -36,10 +36,10 @@ stackit organization member remove SUBJECT [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_organization_role.md b/docs/stackit_organization_role.md index 6be031740..e95171361 100644 --- a/docs/stackit_organization_role.md +++ b/docs/stackit_organization_role.md @@ -21,10 +21,10 @@ stackit organization role [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_organization_role_list.md b/docs/stackit_organization_role_list.md index 265b9967e..13250aa93 100644 --- a/docs/stackit_organization_role_list.md +++ b/docs/stackit_organization_role_list.md @@ -36,10 +36,10 @@ stackit organization role list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex.md b/docs/stackit_postgresflex.md index 125604dea..049c614ca 100644 --- a/docs/stackit_postgresflex.md +++ b/docs/stackit_postgresflex.md @@ -21,10 +21,10 @@ stackit postgresflex [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_backup.md b/docs/stackit_postgresflex_backup.md index 2fcac02b7..667a0d3ac 100644 --- a/docs/stackit_postgresflex_backup.md +++ b/docs/stackit_postgresflex_backup.md @@ -21,10 +21,10 @@ stackit postgresflex backup [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_backup_describe.md b/docs/stackit_postgresflex_backup_describe.md index bc506d775..41b20c98f 100644 --- a/docs/stackit_postgresflex_backup_describe.md +++ b/docs/stackit_postgresflex_backup_describe.md @@ -32,10 +32,10 @@ stackit postgresflex backup describe BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_backup_list.md b/docs/stackit_postgresflex_backup_list.md index fcdc7a536..6c7c61b8a 100644 --- a/docs/stackit_postgresflex_backup_list.md +++ b/docs/stackit_postgresflex_backup_list.md @@ -36,10 +36,10 @@ stackit postgresflex backup list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_backup_update-schedule.md b/docs/stackit_postgresflex_backup_update-schedule.md index 50a369194..fa912af61 100644 --- a/docs/stackit_postgresflex_backup_update-schedule.md +++ b/docs/stackit_postgresflex_backup_update-schedule.md @@ -30,10 +30,10 @@ stackit postgresflex backup update-schedule [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance.md b/docs/stackit_postgresflex_instance.md index bfd19a6c9..4acb0d07b 100644 --- a/docs/stackit_postgresflex_instance.md +++ b/docs/stackit_postgresflex_instance.md @@ -21,10 +21,10 @@ stackit postgresflex instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance_clone.md b/docs/stackit_postgresflex_instance_clone.md index ce0203986..a435dd61e 100644 --- a/docs/stackit_postgresflex_instance_clone.md +++ b/docs/stackit_postgresflex_instance_clone.md @@ -37,10 +37,10 @@ stackit postgresflex instance clone INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance_create.md b/docs/stackit_postgresflex_instance_create.md index b4a333ba3..65150d045 100644 --- a/docs/stackit_postgresflex_instance_create.md +++ b/docs/stackit_postgresflex_instance_create.md @@ -35,7 +35,7 @@ stackit postgresflex instance create [flags] --ram int Amount of RAM (in GB) --storage-class string Storage class (default "premium-perf2-stackit") --storage-size int Storage size (in GB) (default 10) - --type string Instance type, one of ["Replica" "Single"] (default "Replica") + --type string Instance type, (one of: [Replica, Single]) (default "Replica") --version string PostgreSQL version. Defaults to the latest version available ``` @@ -44,10 +44,10 @@ stackit postgresflex instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance_delete.md b/docs/stackit_postgresflex_instance_delete.md index 17992e627..c652d3745 100644 --- a/docs/stackit_postgresflex_instance_delete.md +++ b/docs/stackit_postgresflex_instance_delete.md @@ -34,10 +34,10 @@ stackit postgresflex instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance_describe.md b/docs/stackit_postgresflex_instance_describe.md index 629ffbc9b..7b50cb524 100644 --- a/docs/stackit_postgresflex_instance_describe.md +++ b/docs/stackit_postgresflex_instance_describe.md @@ -31,10 +31,10 @@ stackit postgresflex instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance_list.md b/docs/stackit_postgresflex_instance_list.md index 9d452376b..98c80590b 100644 --- a/docs/stackit_postgresflex_instance_list.md +++ b/docs/stackit_postgresflex_instance_list.md @@ -35,10 +35,10 @@ stackit postgresflex instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_instance_update.md b/docs/stackit_postgresflex_instance_update.md index 844e7d22a..511ccbc9c 100644 --- a/docs/stackit_postgresflex_instance_update.md +++ b/docs/stackit_postgresflex_instance_update.md @@ -32,7 +32,7 @@ stackit postgresflex instance update INSTANCE_ID [flags] --ram int Amount of RAM (in GB) --storage-class string Storage class --storage-size int Storage size (in GB) - --type string Instance type, one of ["Replica" "Single"] + --type string Instance type, (one of: [Replica, Single]) --version string Version ``` @@ -41,10 +41,10 @@ stackit postgresflex instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_options.md b/docs/stackit_postgresflex_options.md index 7fa8ee2bd..d6dc73d98 100644 --- a/docs/stackit_postgresflex_options.md +++ b/docs/stackit_postgresflex_options.md @@ -39,10 +39,10 @@ stackit postgresflex options [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user.md b/docs/stackit_postgresflex_user.md index b1793c663..af06124d4 100644 --- a/docs/stackit_postgresflex_user.md +++ b/docs/stackit_postgresflex_user.md @@ -21,10 +21,10 @@ stackit postgresflex user [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user_create.md b/docs/stackit_postgresflex_user_create.md index fb66d84c3..918588b81 100644 --- a/docs/stackit_postgresflex_user_create.md +++ b/docs/stackit_postgresflex_user_create.md @@ -28,7 +28,7 @@ stackit postgresflex user create [flags] ``` -h, --help Help for "stackit postgresflex user create" --instance-id string ID of the instance - --role strings Roles of the user, possible values are ["login" "createdb"] (default [login]) + --role strings Roles of the user, (multiple of: [login, createdb]) (default [login]) --username string Username of the user ``` @@ -37,10 +37,10 @@ stackit postgresflex user create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user_delete.md b/docs/stackit_postgresflex_user_delete.md index 2bdd099d7..7c52deca2 100644 --- a/docs/stackit_postgresflex_user_delete.md +++ b/docs/stackit_postgresflex_user_delete.md @@ -31,10 +31,10 @@ stackit postgresflex user delete USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user_describe.md b/docs/stackit_postgresflex_user_describe.md index 365b6764b..fc50454cf 100644 --- a/docs/stackit_postgresflex_user_describe.md +++ b/docs/stackit_postgresflex_user_describe.md @@ -34,10 +34,10 @@ stackit postgresflex user describe USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user_list.md b/docs/stackit_postgresflex_user_list.md index 986fc8567..c2e817675 100644 --- a/docs/stackit_postgresflex_user_list.md +++ b/docs/stackit_postgresflex_user_list.md @@ -36,10 +36,10 @@ stackit postgresflex user list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user_reset-password.md b/docs/stackit_postgresflex_user_reset-password.md index 42216c5e8..d99aeb9e7 100644 --- a/docs/stackit_postgresflex_user_reset-password.md +++ b/docs/stackit_postgresflex_user_reset-password.md @@ -30,10 +30,10 @@ stackit postgresflex user reset-password USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_postgresflex_user_update.md b/docs/stackit_postgresflex_user_update.md index d76b18447..643f2591c 100644 --- a/docs/stackit_postgresflex_user_update.md +++ b/docs/stackit_postgresflex_user_update.md @@ -22,7 +22,7 @@ stackit postgresflex user update USER_ID [flags] ``` -h, --help Help for "stackit postgresflex user update" --instance-id string ID of the instance - --role strings Roles of the user, possible values are ["login" "createdb"] (default []) + --role strings Roles of the user, (multiple of: [login, createdb]) (default []) ``` ### Options inherited from parent commands @@ -30,10 +30,10 @@ stackit postgresflex user update USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project.md b/docs/stackit_project.md index cd2eb8f46..b1fafcb63 100644 --- a/docs/stackit_project.md +++ b/docs/stackit_project.md @@ -22,10 +22,10 @@ stackit project [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_create.md b/docs/stackit_project_create.md index 9cc565f2b..0a7f95f6f 100644 --- a/docs/stackit_project_create.md +++ b/docs/stackit_project_create.md @@ -43,10 +43,10 @@ stackit project create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_delete.md b/docs/stackit_project_delete.md index b6eb31164..5292a0ada 100644 --- a/docs/stackit_project_delete.md +++ b/docs/stackit_project_delete.md @@ -31,10 +31,10 @@ stackit project delete [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_describe.md b/docs/stackit_project_describe.md index 6afced7fc..fded420d1 100644 --- a/docs/stackit_project_describe.md +++ b/docs/stackit_project_describe.md @@ -35,10 +35,10 @@ stackit project describe [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_list.md b/docs/stackit_project_list.md index f4b22b007..202a3008e 100644 --- a/docs/stackit_project_list.md +++ b/docs/stackit_project_list.md @@ -43,10 +43,10 @@ stackit project list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_member.md b/docs/stackit_project_member.md index 445143fe2..4e4f4bef4 100644 --- a/docs/stackit_project_member.md +++ b/docs/stackit_project_member.md @@ -21,10 +21,10 @@ stackit project member [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_member_add.md b/docs/stackit_project_member_add.md index d3fc54513..f39c0a932 100644 --- a/docs/stackit_project_member_add.md +++ b/docs/stackit_project_member_add.md @@ -33,10 +33,10 @@ stackit project member add SUBJECT [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_member_list.md b/docs/stackit_project_member_list.md index 18d7874d8..aeb8efa89 100644 --- a/docs/stackit_project_member_list.md +++ b/docs/stackit_project_member_list.md @@ -28,7 +28,7 @@ stackit project member list [flags] ``` -h, --help Help for "stackit project member list" --limit int Maximum number of entries to list - --sort-by string Sort entries by a specific field, one of ["subject" "role"] (default "subject") + --sort-by string Sort entries by a specific field, (one of: [subject, role]) (default "subject") --subject string Filter by subject (the identifier of a user, service account or client). This is usually the email address (for users) or name (for clients) ``` @@ -37,10 +37,10 @@ stackit project member list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_member_remove.md b/docs/stackit_project_member_remove.md index 908e44523..137a895d1 100644 --- a/docs/stackit_project_member_remove.md +++ b/docs/stackit_project_member_remove.md @@ -35,10 +35,10 @@ stackit project member remove SUBJECT [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_role.md b/docs/stackit_project_role.md index d02d2512d..9d69128f0 100644 --- a/docs/stackit_project_role.md +++ b/docs/stackit_project_role.md @@ -21,10 +21,10 @@ stackit project role [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_role_list.md b/docs/stackit_project_role_list.md index 90c54d0b1..cae7f48cf 100644 --- a/docs/stackit_project_role_list.md +++ b/docs/stackit_project_role_list.md @@ -35,10 +35,10 @@ stackit project role list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_project_update.md b/docs/stackit_project_update.md index 9c64e8f6a..69441ac12 100644 --- a/docs/stackit_project_update.md +++ b/docs/stackit_project_update.md @@ -37,10 +37,10 @@ stackit project update [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip.md b/docs/stackit_public-ip.md index 99bfd899f..569cf7f72 100644 --- a/docs/stackit_public-ip.md +++ b/docs/stackit_public-ip.md @@ -21,10 +21,10 @@ stackit public-ip [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO @@ -36,5 +36,6 @@ stackit public-ip [flags] * [stackit public-ip describe](./stackit_public-ip_describe.md) - Shows details of a Public IP * [stackit public-ip disassociate](./stackit_public-ip_disassociate.md) - Disassociates a Public IP from a network interface or a virtual IP * [stackit public-ip list](./stackit_public-ip_list.md) - Lists all Public IPs of a project +* [stackit public-ip ranges](./stackit_public-ip_ranges.md) - Provides functionality for STACKIT public-ip ranges * [stackit public-ip update](./stackit_public-ip_update.md) - Updates a Public IP diff --git a/docs/stackit_public-ip_associate.md b/docs/stackit_public-ip_associate.md index 484eabaf8..6d1f24a49 100644 --- a/docs/stackit_public-ip_associate.md +++ b/docs/stackit_public-ip_associate.md @@ -29,10 +29,10 @@ stackit public-ip associate PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip_create.md b/docs/stackit_public-ip_create.md index 9ea017de9..fa0a5d592 100644 --- a/docs/stackit_public-ip_create.md +++ b/docs/stackit_public-ip_create.md @@ -36,10 +36,10 @@ stackit public-ip create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip_delete.md b/docs/stackit_public-ip_delete.md index d5d7c46e2..161d07fca 100644 --- a/docs/stackit_public-ip_delete.md +++ b/docs/stackit_public-ip_delete.md @@ -30,10 +30,10 @@ stackit public-ip delete PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip_describe.md b/docs/stackit_public-ip_describe.md index 811d7c4d7..3da3bb8ac 100644 --- a/docs/stackit_public-ip_describe.md +++ b/docs/stackit_public-ip_describe.md @@ -31,10 +31,10 @@ stackit public-ip describe PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip_disassociate.md b/docs/stackit_public-ip_disassociate.md index 992ca2044..73733e35f 100644 --- a/docs/stackit_public-ip_disassociate.md +++ b/docs/stackit_public-ip_disassociate.md @@ -28,10 +28,10 @@ stackit public-ip disassociate PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip_list.md b/docs/stackit_public-ip_list.md index 513394fa9..b4273ef94 100644 --- a/docs/stackit_public-ip_list.md +++ b/docs/stackit_public-ip_list.md @@ -39,10 +39,10 @@ stackit public-ip list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_public-ip_ranges.md b/docs/stackit_public-ip_ranges.md new file mode 100644 index 000000000..70a7971e3 --- /dev/null +++ b/docs/stackit_public-ip_ranges.md @@ -0,0 +1,34 @@ +## stackit public-ip ranges + +Provides functionality for STACKIT public-ip ranges + +### Synopsis + +Provides functionality for STACKIT public-ip ranges + +``` +stackit public-ip ranges [flags] +``` + +### Options + +``` + -h, --help Help for "stackit public-ip ranges" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit public-ip](./stackit_public-ip.md) - Provides functionality for public IPs +* [stackit public-ip ranges list](./stackit_public-ip_ranges_list.md) - Lists all STACKIT public-ip ranges + diff --git a/docs/stackit_public-ip_ranges_list.md b/docs/stackit_public-ip_ranges_list.md new file mode 100644 index 000000000..8085da895 --- /dev/null +++ b/docs/stackit_public-ip_ranges_list.md @@ -0,0 +1,47 @@ +## stackit public-ip ranges list + +Lists all STACKIT public-ip ranges + +### Synopsis + +Lists all STACKIT public-ip ranges. + +``` +stackit public-ip ranges list [flags] +``` + +### Examples + +``` + Lists all STACKIT public-ip ranges + $ stackit public-ip ranges list + + Lists all STACKIT public-ip ranges, piping to a tool like fzf for interactive selection + $ stackit public-ip ranges list -o pretty | fzf + + Lists up to 10 STACKIT public-ip ranges + $ stackit public-ip ranges list --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit public-ip ranges list" + --limit int Maximum number of entries to list +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit public-ip ranges](./stackit_public-ip_ranges.md) - Provides functionality for STACKIT public-ip ranges + diff --git a/docs/stackit_public-ip_update.md b/docs/stackit_public-ip_update.md index 96a625987..8160b1a7e 100644 --- a/docs/stackit_public-ip_update.md +++ b/docs/stackit_public-ip_update.md @@ -32,10 +32,10 @@ stackit public-ip update PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_quota.md b/docs/stackit_quota.md index 074b95cbc..f0c5abc3a 100644 --- a/docs/stackit_quota.md +++ b/docs/stackit_quota.md @@ -21,10 +21,10 @@ stackit quota [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_quota_list.md b/docs/stackit_quota_list.md index f68113391..5fc69d003 100644 --- a/docs/stackit_quota_list.md +++ b/docs/stackit_quota_list.md @@ -28,10 +28,10 @@ stackit quota list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq.md b/docs/stackit_rabbitmq.md index 2e6779ccc..f9a9e114c 100644 --- a/docs/stackit_rabbitmq.md +++ b/docs/stackit_rabbitmq.md @@ -21,10 +21,10 @@ stackit rabbitmq [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_credentials.md b/docs/stackit_rabbitmq_credentials.md index debec8700..04020705c 100644 --- a/docs/stackit_rabbitmq_credentials.md +++ b/docs/stackit_rabbitmq_credentials.md @@ -21,10 +21,10 @@ stackit rabbitmq credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_credentials_create.md b/docs/stackit_rabbitmq_credentials_create.md index 98f1c8e4c..73f15ff9d 100644 --- a/docs/stackit_rabbitmq_credentials_create.md +++ b/docs/stackit_rabbitmq_credentials_create.md @@ -33,10 +33,10 @@ stackit rabbitmq credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_credentials_delete.md b/docs/stackit_rabbitmq_credentials_delete.md index 9e9cae18e..1e6065af2 100644 --- a/docs/stackit_rabbitmq_credentials_delete.md +++ b/docs/stackit_rabbitmq_credentials_delete.md @@ -29,10 +29,10 @@ stackit rabbitmq credentials delete CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_credentials_describe.md b/docs/stackit_rabbitmq_credentials_describe.md index bd1f9fad3..067255b88 100644 --- a/docs/stackit_rabbitmq_credentials_describe.md +++ b/docs/stackit_rabbitmq_credentials_describe.md @@ -32,10 +32,10 @@ stackit rabbitmq credentials describe CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_credentials_list.md b/docs/stackit_rabbitmq_credentials_list.md index 8fe0b14f6..4ca3aa9e2 100644 --- a/docs/stackit_rabbitmq_credentials_list.md +++ b/docs/stackit_rabbitmq_credentials_list.md @@ -36,10 +36,10 @@ stackit rabbitmq credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_instance.md b/docs/stackit_rabbitmq_instance.md index 0c6dd7af3..c95d19a29 100644 --- a/docs/stackit_rabbitmq_instance.md +++ b/docs/stackit_rabbitmq_instance.md @@ -21,10 +21,10 @@ stackit rabbitmq instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_instance_create.md b/docs/stackit_rabbitmq_instance_create.md index 8b00f0fcc..92d74caa0 100644 --- a/docs/stackit_rabbitmq_instance_create.md +++ b/docs/stackit_rabbitmq_instance_create.md @@ -30,13 +30,13 @@ stackit rabbitmq instance create [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit rabbitmq instance create" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID -n, --name string Instance name --plan-id string Plan ID --plan-name string Plan name - --plugin strings Plugin + --plugin strings Plugins (multiple of: [rabbitmq_consistent_hash_exchange, rabbitmq_federation, rabbitmq_federation_management, rabbitmq_mqtt, rabbitmq_sharding, rabbitmq_shovel, rabbitmq_shovel_management, rabbitmq_stomp, rabbitmq_tracing, rabbitmq_event_exchange]) (default []) --syslog strings Syslog --version string Instance RabbitMQ version ``` @@ -46,10 +46,10 @@ stackit rabbitmq instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_instance_delete.md b/docs/stackit_rabbitmq_instance_delete.md index 4a078fc5e..9ab5801f3 100644 --- a/docs/stackit_rabbitmq_instance_delete.md +++ b/docs/stackit_rabbitmq_instance_delete.md @@ -28,10 +28,10 @@ stackit rabbitmq instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_instance_describe.md b/docs/stackit_rabbitmq_instance_describe.md index e2cb8cb74..bc17a046b 100644 --- a/docs/stackit_rabbitmq_instance_describe.md +++ b/docs/stackit_rabbitmq_instance_describe.md @@ -31,10 +31,10 @@ stackit rabbitmq instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_instance_list.md b/docs/stackit_rabbitmq_instance_list.md index 196a8f275..d834200cc 100644 --- a/docs/stackit_rabbitmq_instance_list.md +++ b/docs/stackit_rabbitmq_instance_list.md @@ -35,10 +35,10 @@ stackit rabbitmq instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_instance_update.md b/docs/stackit_rabbitmq_instance_update.md index 985781786..1cc196bae 100644 --- a/docs/stackit_rabbitmq_instance_update.md +++ b/docs/stackit_rabbitmq_instance_update.md @@ -27,12 +27,12 @@ stackit rabbitmq instance update INSTANCE_ID [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit rabbitmq instance update" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID --plan-id string Plan ID --plan-name string Plan name - --plugin strings Plugin + --plugin strings Plugins (multiple of: [rabbitmq_consistent_hash_exchange, rabbitmq_federation, rabbitmq_federation_management, rabbitmq_mqtt, rabbitmq_sharding, rabbitmq_shovel, rabbitmq_shovel_management, rabbitmq_stomp, rabbitmq_tracing, rabbitmq_event_exchange]) (default []) --syslog strings Syslog --version string Instance RabbitMQ version ``` @@ -42,10 +42,10 @@ stackit rabbitmq instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_rabbitmq_plans.md b/docs/stackit_rabbitmq_plans.md index fe33723a0..565b56f89 100644 --- a/docs/stackit_rabbitmq_plans.md +++ b/docs/stackit_rabbitmq_plans.md @@ -35,10 +35,10 @@ stackit rabbitmq plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis.md b/docs/stackit_redis.md index b371d2389..1afc75205 100644 --- a/docs/stackit_redis.md +++ b/docs/stackit_redis.md @@ -21,10 +21,10 @@ stackit redis [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_credentials.md b/docs/stackit_redis_credentials.md index 323147b4b..5002442de 100644 --- a/docs/stackit_redis_credentials.md +++ b/docs/stackit_redis_credentials.md @@ -21,10 +21,10 @@ stackit redis credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_credentials_create.md b/docs/stackit_redis_credentials_create.md index 444f9ee1e..949cb2163 100644 --- a/docs/stackit_redis_credentials_create.md +++ b/docs/stackit_redis_credentials_create.md @@ -33,10 +33,10 @@ stackit redis credentials create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_credentials_delete.md b/docs/stackit_redis_credentials_delete.md index 2ff4795cf..702f80e1c 100644 --- a/docs/stackit_redis_credentials_delete.md +++ b/docs/stackit_redis_credentials_delete.md @@ -29,10 +29,10 @@ stackit redis credentials delete CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_credentials_describe.md b/docs/stackit_redis_credentials_describe.md index 8c3937344..06a8a7cef 100644 --- a/docs/stackit_redis_credentials_describe.md +++ b/docs/stackit_redis_credentials_describe.md @@ -32,10 +32,10 @@ stackit redis credentials describe CREDENTIALS_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_credentials_list.md b/docs/stackit_redis_credentials_list.md index 14c162c0b..4eefcd466 100644 --- a/docs/stackit_redis_credentials_list.md +++ b/docs/stackit_redis_credentials_list.md @@ -36,10 +36,10 @@ stackit redis credentials list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_instance.md b/docs/stackit_redis_instance.md index 009415f9b..03c044a49 100644 --- a/docs/stackit_redis_instance.md +++ b/docs/stackit_redis_instance.md @@ -21,10 +21,10 @@ stackit redis instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_instance_create.md b/docs/stackit_redis_instance_create.md index 004741d48..590138129 100644 --- a/docs/stackit_redis_instance_create.md +++ b/docs/stackit_redis_instance_create.md @@ -30,7 +30,7 @@ stackit redis instance create [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit redis instance create" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID -n, --name string Instance name @@ -45,10 +45,10 @@ stackit redis instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_instance_delete.md b/docs/stackit_redis_instance_delete.md index 2508659ee..02383d8b8 100644 --- a/docs/stackit_redis_instance_delete.md +++ b/docs/stackit_redis_instance_delete.md @@ -28,10 +28,10 @@ stackit redis instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_instance_describe.md b/docs/stackit_redis_instance_describe.md index 4f7632ca3..426c10631 100644 --- a/docs/stackit_redis_instance_describe.md +++ b/docs/stackit_redis_instance_describe.md @@ -31,10 +31,10 @@ stackit redis instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_instance_list.md b/docs/stackit_redis_instance_list.md index 3c97031ac..aeb5a0963 100644 --- a/docs/stackit_redis_instance_list.md +++ b/docs/stackit_redis_instance_list.md @@ -35,10 +35,10 @@ stackit redis instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_instance_update.md b/docs/stackit_redis_instance_update.md index 801622ed7..362b7e37b 100644 --- a/docs/stackit_redis_instance_update.md +++ b/docs/stackit_redis_instance_update.md @@ -27,7 +27,7 @@ stackit redis instance update INSTANCE_ID [flags] --enable-monitoring Enable monitoring --graphite string Graphite host -h, --help Help for "stackit redis instance update" - --metrics-frequency int Metrics frequency + --metrics-frequency int32 Metrics frequency --metrics-prefix string Metrics prefix --monitoring-instance-id string Monitoring instance ID --plan-id string Plan ID @@ -41,10 +41,10 @@ stackit redis instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_redis_plans.md b/docs/stackit_redis_plans.md index 2dc40245a..1a88287b3 100644 --- a/docs/stackit_redis_plans.md +++ b/docs/stackit_redis_plans.md @@ -35,10 +35,10 @@ stackit redis plans [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager.md b/docs/stackit_secrets-manager.md index 84bb95a52..d72dfad24 100644 --- a/docs/stackit_secrets-manager.md +++ b/docs/stackit_secrets-manager.md @@ -21,10 +21,10 @@ stackit secrets-manager [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_instance.md b/docs/stackit_secrets-manager_instance.md index db579c80c..6a491b36b 100644 --- a/docs/stackit_secrets-manager_instance.md +++ b/docs/stackit_secrets-manager_instance.md @@ -21,10 +21,10 @@ stackit secrets-manager instance [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_instance_create.md b/docs/stackit_secrets-manager_instance_create.md index 379de7785..7de5fd5f0 100644 --- a/docs/stackit_secrets-manager_instance_create.md +++ b/docs/stackit_secrets-manager_instance_create.md @@ -18,14 +18,21 @@ stackit secrets-manager instance create [flags] Create a Secrets Manager instance with name "my-instance" and specify IP range which is allowed to access it $ stackit secrets-manager instance create --name my-instance --acl 1.2.3.0/24 + + Create a Secrets Manager instance with name "my-instance" and configure KMS key options + $ stackit secrets-manager instance create --name my-instance --kms-key-id key-id --kms-keyring-id keyring-id --kms-key-version 1 --kms-service-account-email my-service-account-1234567@sa.stackit.cloud ``` ### Options ``` - --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) - -h, --help Help for "stackit secrets-manager instance create" - -n, --name string Instance name + --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) + -h, --help Help for "stackit secrets-manager instance create" + --kms-key-id string ID of the KMS key to use for encryption + --kms-key-version int Version of the KMS key + --kms-keyring-id string ID of the KMS key ring + --kms-service-account-email string Service account email for KMS access + -n, --name string Instance name ``` ### Options inherited from parent commands @@ -33,10 +40,10 @@ stackit secrets-manager instance create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_instance_delete.md b/docs/stackit_secrets-manager_instance_delete.md index 0a8b18c6b..7b8aa1155 100644 --- a/docs/stackit_secrets-manager_instance_delete.md +++ b/docs/stackit_secrets-manager_instance_delete.md @@ -28,10 +28,10 @@ stackit secrets-manager instance delete INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_instance_describe.md b/docs/stackit_secrets-manager_instance_describe.md index d2695dc76..5b499b2fc 100644 --- a/docs/stackit_secrets-manager_instance_describe.md +++ b/docs/stackit_secrets-manager_instance_describe.md @@ -31,10 +31,10 @@ stackit secrets-manager instance describe INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_instance_list.md b/docs/stackit_secrets-manager_instance_list.md index 742dd9000..74f957b50 100644 --- a/docs/stackit_secrets-manager_instance_list.md +++ b/docs/stackit_secrets-manager_instance_list.md @@ -35,10 +35,10 @@ stackit secrets-manager instance list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_instance_update.md b/docs/stackit_secrets-manager_instance_update.md index cf40d3c1a..a0fb1a29a 100644 --- a/docs/stackit_secrets-manager_instance_update.md +++ b/docs/stackit_secrets-manager_instance_update.md @@ -13,15 +13,29 @@ stackit secrets-manager instance update INSTANCE_ID [flags] ### Examples ``` + Update the name of a Secrets Manager instance with ID "xxx" + $ stackit secrets-manager instance update xxx --name my-new-name + Update the range of IPs allowed to access a Secrets Manager instance with ID "xxx" $ stackit secrets-manager instance update xxx --acl 1.2.3.0/24 + + Update the name and ACLs of a Secrets Manager instance with ID "xxx" + $ stackit secrets-manager instance update xxx --name my-new-name --acl 1.2.3.0/24 + + Update the KMS key settings of a Secrets Manager instance with ID "xxx" + $ stackit secrets-manager instance update xxx --name my-instance --kms-key-id key-id --kms-keyring-id keyring-id --kms-key-version 1 --kms-service-account-email my-service-account-1234567@sa.stackit.cloud ``` ### Options ``` - --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) - -h, --help Help for "stackit secrets-manager instance update" + --acl strings List of IP networks in CIDR notation which are allowed to access this instance (default []) + -h, --help Help for "stackit secrets-manager instance update" + --kms-key-id string ID of the KMS key to use for encryption + --kms-key-version int Version of the KMS key + --kms-keyring-id string ID of the KMS key ring + --kms-service-account-email string Service account email for KMS access + -n, --name string Instance name ``` ### Options inherited from parent commands @@ -29,10 +43,10 @@ stackit secrets-manager instance update INSTANCE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_user.md b/docs/stackit_secrets-manager_user.md index b6ba33cae..2e69e0c9c 100644 --- a/docs/stackit_secrets-manager_user.md +++ b/docs/stackit_secrets-manager_user.md @@ -21,10 +21,10 @@ stackit secrets-manager user [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_user_create.md b/docs/stackit_secrets-manager_user_create.md index 34f83c324..629811420 100644 --- a/docs/stackit_secrets-manager_user_create.md +++ b/docs/stackit_secrets-manager_user_create.md @@ -36,10 +36,10 @@ stackit secrets-manager user create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_user_delete.md b/docs/stackit_secrets-manager_user_delete.md index eb2a28000..a4ab60a94 100644 --- a/docs/stackit_secrets-manager_user_delete.md +++ b/docs/stackit_secrets-manager_user_delete.md @@ -30,10 +30,10 @@ stackit secrets-manager user delete USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_user_describe.md b/docs/stackit_secrets-manager_user_describe.md index 56ca9f25b..a67669a87 100644 --- a/docs/stackit_secrets-manager_user_describe.md +++ b/docs/stackit_secrets-manager_user_describe.md @@ -32,10 +32,10 @@ stackit secrets-manager user describe USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_user_list.md b/docs/stackit_secrets-manager_user_list.md index b610591be..4f1f3ad99 100644 --- a/docs/stackit_secrets-manager_user_list.md +++ b/docs/stackit_secrets-manager_user_list.md @@ -36,10 +36,10 @@ stackit secrets-manager user list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_secrets-manager_user_update.md b/docs/stackit_secrets-manager_user_update.md index c181eb96e..d001b9657 100644 --- a/docs/stackit_secrets-manager_user_update.md +++ b/docs/stackit_secrets-manager_user_update.md @@ -34,10 +34,10 @@ stackit secrets-manager user update USER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group.md b/docs/stackit_security-group.md index 949333c96..42b36afed 100644 --- a/docs/stackit_security-group.md +++ b/docs/stackit_security-group.md @@ -21,10 +21,10 @@ stackit security-group [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_create.md b/docs/stackit_security-group_create.md index c63370118..969814d73 100644 --- a/docs/stackit_security-group_create.md +++ b/docs/stackit_security-group_create.md @@ -35,10 +35,10 @@ stackit security-group create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_delete.md b/docs/stackit_security-group_delete.md index 6402e0bd2..1eb103da3 100644 --- a/docs/stackit_security-group_delete.md +++ b/docs/stackit_security-group_delete.md @@ -28,10 +28,10 @@ stackit security-group delete GROUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_describe.md b/docs/stackit_security-group_describe.md index 2a29d26fc..6e30a3add 100644 --- a/docs/stackit_security-group_describe.md +++ b/docs/stackit_security-group_describe.md @@ -28,10 +28,10 @@ stackit security-group describe GROUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_list.md b/docs/stackit_security-group_list.md index 990f01364..2f33177c2 100644 --- a/docs/stackit_security-group_list.md +++ b/docs/stackit_security-group_list.md @@ -13,11 +13,17 @@ stackit security-group list [flags] ### Examples ``` - List all groups + Lists all security groups $ stackit security-group list - List groups with labels + Lists security groups with labels $ stackit security-group list --label-selector label1=value1,label2=value2 + + Lists all security groups in JSON format + $ stackit security-group list --output-format json + + Lists up to 10 security groups + $ stackit security-group list --limit 10 ``` ### Options @@ -25,6 +31,7 @@ stackit security-group list [flags] ``` -h, --help Help for "stackit security-group list" --label-selector string Filter by label + --limit int Maximum number of entries to list ``` ### Options inherited from parent commands @@ -32,10 +39,10 @@ stackit security-group list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_rule.md b/docs/stackit_security-group_rule.md index 558abe544..7c8d32396 100644 --- a/docs/stackit_security-group_rule.md +++ b/docs/stackit_security-group_rule.md @@ -21,10 +21,10 @@ stackit security-group rule [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_rule_create.md b/docs/stackit_security-group_rule_create.md index 0ad7a823b..90ecbdfdf 100644 --- a/docs/stackit_security-group_rule_create.md +++ b/docs/stackit_security-group_rule_create.md @@ -49,10 +49,10 @@ stackit security-group rule create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_rule_delete.md b/docs/stackit_security-group_rule_delete.md index 003912835..d130759e9 100644 --- a/docs/stackit_security-group_rule_delete.md +++ b/docs/stackit_security-group_rule_delete.md @@ -31,10 +31,10 @@ stackit security-group rule delete SECURITY_GROUP_RULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_rule_describe.md b/docs/stackit_security-group_rule_describe.md index 66579d57e..0b016815a 100644 --- a/docs/stackit_security-group_rule_describe.md +++ b/docs/stackit_security-group_rule_describe.md @@ -32,10 +32,10 @@ stackit security-group rule describe SECURITY_GROUP_RULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_rule_list.md b/docs/stackit_security-group_rule_list.md index c1aff833b..93701b157 100644 --- a/docs/stackit_security-group_rule_list.md +++ b/docs/stackit_security-group_rule_list.md @@ -36,10 +36,10 @@ stackit security-group rule list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_security-group_update.md b/docs/stackit_security-group_update.md index 4dd30f03b..0240a1550 100644 --- a/docs/stackit_security-group_update.md +++ b/docs/stackit_security-group_update.md @@ -34,10 +34,10 @@ stackit security-group update GROUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server.md b/docs/stackit_server.md index 83bf55541..555ef00ac 100644 --- a/docs/stackit_server.md +++ b/docs/stackit_server.md @@ -21,10 +21,10 @@ stackit server [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO @@ -46,6 +46,7 @@ stackit server [flags] * [stackit server reboot](./stackit_server_reboot.md) - Reboots a server * [stackit server rescue](./stackit_server_rescue.md) - Rescues an existing server * [stackit server resize](./stackit_server_resize.md) - Resizes the server to the given machine type +* [stackit server security-group](./stackit_server_security-group.md) - Allows attaching/detaching security groups to servers * [stackit server service-account](./stackit_server_service-account.md) - Allows attaching/detaching service accounts to servers * [stackit server start](./stackit_server_start.md) - Starts an existing server or allocates the server if deallocated * [stackit server stop](./stackit_server_stop.md) - Stops an existing server diff --git a/docs/stackit_server_backup.md b/docs/stackit_server_backup.md index 40ec81ed9..7ad7ea4a1 100644 --- a/docs/stackit_server_backup.md +++ b/docs/stackit_server_backup.md @@ -21,10 +21,10 @@ stackit server backup [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_create.md b/docs/stackit_server_backup_create.md index 0d77984fb..7adfa52d1 100644 --- a/docs/stackit_server_backup_create.md +++ b/docs/stackit_server_backup_create.md @@ -23,11 +23,11 @@ stackit server backup create [flags] ### Options ``` - -h, --help Help for "stackit server backup create" - -b, --name string Backup name - -d, --retention-period int Backup retention period (in days) (default 14) - -s, --server-id string Server ID - -i, --volume-ids strings Backup volume IDs, as comma separated UUID values. (default []) + -h, --help Help for "stackit server backup create" + -b, --name string Backup name + -d, --retention-period int32 Backup retention period (in days) (default 14) + -s, --server-id string Server ID + -i, --volume-ids strings Backup volume IDs, as comma separated UUID values. (default []) ``` ### Options inherited from parent commands @@ -35,10 +35,10 @@ stackit server backup create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_delete.md b/docs/stackit_server_backup_delete.md index 96e1fca84..214ddbfc4 100644 --- a/docs/stackit_server_backup_delete.md +++ b/docs/stackit_server_backup_delete.md @@ -29,10 +29,10 @@ stackit server backup delete BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_describe.md b/docs/stackit_server_backup_describe.md index 008fc02ee..50887ab7f 100644 --- a/docs/stackit_server_backup_describe.md +++ b/docs/stackit_server_backup_describe.md @@ -32,10 +32,10 @@ stackit server backup describe BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_disable.md b/docs/stackit_server_backup_disable.md index 3a5d623d1..89f6389f8 100644 --- a/docs/stackit_server_backup_disable.md +++ b/docs/stackit_server_backup_disable.md @@ -29,10 +29,10 @@ stackit server backup disable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_enable.md b/docs/stackit_server_backup_enable.md index e0268a57f..5813bf387 100644 --- a/docs/stackit_server_backup_enable.md +++ b/docs/stackit_server_backup_enable.md @@ -29,10 +29,10 @@ stackit server backup enable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_list.md b/docs/stackit_server_backup_list.md index 702d4917b..c2e66bd9b 100644 --- a/docs/stackit_server_backup_list.md +++ b/docs/stackit_server_backup_list.md @@ -33,10 +33,10 @@ stackit server backup list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_restore.md b/docs/stackit_server_backup_restore.md index 1b33b16f6..f1339eed1 100644 --- a/docs/stackit_server_backup_restore.md +++ b/docs/stackit_server_backup_restore.md @@ -34,10 +34,10 @@ stackit server backup restore BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_schedule.md b/docs/stackit_server_backup_schedule.md index 710c97b18..00c4ee68a 100644 --- a/docs/stackit_server_backup_schedule.md +++ b/docs/stackit_server_backup_schedule.md @@ -21,10 +21,10 @@ stackit server backup schedule [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_schedule_create.md b/docs/stackit_server_backup_schedule_create.md index 8b0460852..dbda024be 100644 --- a/docs/stackit_server_backup_schedule_create.md +++ b/docs/stackit_server_backup_schedule_create.md @@ -23,14 +23,14 @@ stackit server backup schedule create [flags] ### Options ``` - -b, --backup-name string Backup name - -d, --backup-retention-period int Backup retention period (in days) (default 14) - -n, --backup-schedule-name string Backup schedule name - -i, --backup-volume-ids strings Backup volume IDs, as comma separated UUID values. (default []) - -e, --enabled Is the server backup schedule enabled (default true) - -h, --help Help for "stackit server backup schedule create" - -r, --rrule string Backup RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") - -s, --server-id string Server ID + -b, --backup-name string Backup name + -d, --backup-retention-period int32 Backup retention period (in days) (default 14) + -n, --backup-schedule-name string Backup schedule name + -i, --backup-volume-ids strings Backup volume IDs, as comma separated UUID values. (default []) + -e, --enabled Is the server backup schedule enabled (default true) + -h, --help Help for "stackit server backup schedule create" + -r, --rrule string Backup RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") + -s, --server-id string Server ID ``` ### Options inherited from parent commands @@ -38,10 +38,10 @@ stackit server backup schedule create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_schedule_delete.md b/docs/stackit_server_backup_schedule_delete.md index e4fbf501f..e1dc97b6e 100644 --- a/docs/stackit_server_backup_schedule_delete.md +++ b/docs/stackit_server_backup_schedule_delete.md @@ -29,10 +29,10 @@ stackit server backup schedule delete SCHEDULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_schedule_describe.md b/docs/stackit_server_backup_schedule_describe.md index e90933b67..d40e7bd0a 100644 --- a/docs/stackit_server_backup_schedule_describe.md +++ b/docs/stackit_server_backup_schedule_describe.md @@ -32,10 +32,10 @@ stackit server backup schedule describe BACKUP_SCHEDULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_schedule_list.md b/docs/stackit_server_backup_schedule_list.md index e5c69b1ad..71ea6cf98 100644 --- a/docs/stackit_server_backup_schedule_list.md +++ b/docs/stackit_server_backup_schedule_list.md @@ -33,10 +33,10 @@ stackit server backup schedule list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_schedule_update.md b/docs/stackit_server_backup_schedule_update.md index 522c6e7a7..f6e5bc921 100644 --- a/docs/stackit_server_backup_schedule_update.md +++ b/docs/stackit_server_backup_schedule_update.md @@ -23,14 +23,14 @@ stackit server backup schedule update SCHEDULE_ID [flags] ### Options ``` - -b, --backup-name string Backup name - -d, --backup-retention-period int Backup retention period (in days) (default 14) - -n, --backup-schedule-name string Backup schedule name - -i, --backup-volume-ids strings Backup volume IDs, as comma separated UUID values. (default []) - -e, --enabled Is the server backup schedule enabled (default true) - -h, --help Help for "stackit server backup schedule update" - -r, --rrule string Backup RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") - -s, --server-id string Server ID + -b, --backup-name string Backup name + -d, --backup-retention-period int32 Backup retention period (in days) (default 14) + -n, --backup-schedule-name string Backup schedule name + -i, --backup-volume-ids strings Backup volume IDs, as comma separated UUID values. (default []) + -e, --enabled Is the server backup schedule enabled (default true) + -h, --help Help for "stackit server backup schedule update" + -r, --rrule string Backup RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") + -s, --server-id string Server ID ``` ### Options inherited from parent commands @@ -38,10 +38,10 @@ stackit server backup schedule update SCHEDULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_volume-backup.md b/docs/stackit_server_backup_volume-backup.md index ba8068b93..7574f3999 100644 --- a/docs/stackit_server_backup_volume-backup.md +++ b/docs/stackit_server_backup_volume-backup.md @@ -21,10 +21,10 @@ stackit server backup volume-backup [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_volume-backup_delete.md b/docs/stackit_server_backup_volume-backup_delete.md index 9cbbdc727..2dcaa2df7 100644 --- a/docs/stackit_server_backup_volume-backup_delete.md +++ b/docs/stackit_server_backup_volume-backup_delete.md @@ -30,10 +30,10 @@ stackit server backup volume-backup delete VOLUME_BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_backup_volume-backup_restore.md b/docs/stackit_server_backup_volume-backup_restore.md index 622d45f6f..e9d30413f 100644 --- a/docs/stackit_server_backup_volume-backup_restore.md +++ b/docs/stackit_server_backup_volume-backup_restore.md @@ -31,10 +31,10 @@ stackit server backup volume-backup restore VOLUME_BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command.md b/docs/stackit_server_command.md index c0640ba60..59b474d0e 100644 --- a/docs/stackit_server_command.md +++ b/docs/stackit_server_command.md @@ -21,10 +21,10 @@ stackit server command [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command_create.md b/docs/stackit_server_command_create.md index 224e7742f..3bb634a67 100644 --- a/docs/stackit_server_command_create.md +++ b/docs/stackit_server_command_create.md @@ -34,10 +34,10 @@ stackit server command create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command_describe.md b/docs/stackit_server_command_describe.md index 61af4782c..4ea803d98 100644 --- a/docs/stackit_server_command_describe.md +++ b/docs/stackit_server_command_describe.md @@ -32,10 +32,10 @@ stackit server command describe COMMAND_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command_list.md b/docs/stackit_server_command_list.md index 6467de601..f5c0b2aff 100644 --- a/docs/stackit_server_command_list.md +++ b/docs/stackit_server_command_list.md @@ -33,10 +33,10 @@ stackit server command list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command_template.md b/docs/stackit_server_command_template.md index 92a904fea..17a293693 100644 --- a/docs/stackit_server_command_template.md +++ b/docs/stackit_server_command_template.md @@ -21,10 +21,10 @@ stackit server command template [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command_template_describe.md b/docs/stackit_server_command_template_describe.md index 86a035a2b..871b979ce 100644 --- a/docs/stackit_server_command_template_describe.md +++ b/docs/stackit_server_command_template_describe.md @@ -32,10 +32,10 @@ stackit server command template describe COMMAND_TEMPLATE_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_command_template_list.md b/docs/stackit_server_command_template_list.md index 36457e6f1..5bc14adf5 100644 --- a/docs/stackit_server_command_template_list.md +++ b/docs/stackit_server_command_template_list.md @@ -32,10 +32,10 @@ stackit server command template list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_console.md b/docs/stackit_server_console.md index a8f6300a6..6b1626f30 100644 --- a/docs/stackit_server_console.md +++ b/docs/stackit_server_console.md @@ -31,10 +31,10 @@ stackit server console SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_create.md b/docs/stackit_server_create.md index 23378c5fa..c459a4c63 100644 --- a/docs/stackit_server_create.md +++ b/docs/stackit_server_create.md @@ -13,38 +13,33 @@ stackit server create [flags] ### Examples ``` - Create a server from an image with id xxx - $ stackit server create --machine-type t1.1 --name server1 --image-id xxx - - Create a server with labels from an image with id xxx - $ stackit server create --machine-type t1.1 --name server1 --image-id xxx --labels key=value,foo=bar - - Create a server with a boot volume - $ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 + Create a server with a boot volume with source type image + $ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 Create a server with a boot volume from an existing volume - $ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type volume + $ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type volume Create a server with a keypair - $ stackit server create --machine-type t1.1 --name server1 --image-id xxx --keypair-name example - - Create a server with a network - $ stackit server create --machine-type t1.1 --name server1 --image-id xxx --network-id yyy + $ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --keypair-name example Create a server with a network interface - $ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --network-interface-ids yyy + $ stackit server create --machine-type g2i.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --network-interface-ids yyy Create a server with an attached volume - $ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --volumes yyy + $ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --volumes zzz Create a server with user data (cloud-init) - $ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --user-data @path/to/file.yaml") + $ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --user-data @path/to/file.yaml + + Create a server with provisioned agent + $ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --agent-provisioning-policy ALWAYS ``` ### Options ``` --affinity-group string The affinity group the server is assigned to + --agent-provisioning-policy string Whether to provision an agent on server creation, (one of: [ALWAYS, NEVER, INHERIT]) (default "INHERIT") --availability-zone string The availability zone of the server --boot-volume-delete-on-termination Delete the volume during the termination of the server. Defaults to false --boot-volume-performance-class string Boot volume performance class @@ -52,10 +47,9 @@ stackit server create [flags] --boot-volume-source-id string ID of the source object of boot volume. It can be either an image or volume ID --boot-volume-source-type string Type of the source object of boot volume. It can be either 'image' or 'volume' -h, --help Help for "stackit server create" - --image-id string The image ID to be used for an ephemeral disk on the server. Either 'image-id' or 'boot-volume-...' flags are required --keypair-name string The name of the SSH keypair used during the server creation --labels stringToString Labels are key-value string pairs which can be attached to a server. E.g. '--labels key1=value1,key2=value2,...' (default []) - --machine-type string Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html + --machine-type string Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/ -n, --name string Server name --network-id string ID of the network for the initial networking setup for the server creation --network-interface-ids strings List of network interface IDs for the initial networking setup for the server creation @@ -70,10 +64,10 @@ stackit server create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_deallocate.md b/docs/stackit_server_deallocate.md index aa4921589..25aa67db9 100644 --- a/docs/stackit_server_deallocate.md +++ b/docs/stackit_server_deallocate.md @@ -28,10 +28,10 @@ stackit server deallocate SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_delete.md b/docs/stackit_server_delete.md index 32cf0bfe5..3a3a32155 100644 --- a/docs/stackit_server_delete.md +++ b/docs/stackit_server_delete.md @@ -30,10 +30,10 @@ stackit server delete SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_describe.md b/docs/stackit_server_describe.md index c6507dfbe..2ae8882bd 100644 --- a/docs/stackit_server_describe.md +++ b/docs/stackit_server_describe.md @@ -31,10 +31,10 @@ stackit server describe SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_list.md b/docs/stackit_server_list.md index 064850236..703b7e5bc 100644 --- a/docs/stackit_server_list.md +++ b/docs/stackit_server_list.md @@ -39,10 +39,10 @@ stackit server list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_log.md b/docs/stackit_server_log.md index c1e1e7975..7cde020a4 100644 --- a/docs/stackit_server_log.md +++ b/docs/stackit_server_log.md @@ -35,10 +35,10 @@ stackit server log SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_machine-type.md b/docs/stackit_server_machine-type.md index 4a7058bd8..04d80923f 100644 --- a/docs/stackit_server_machine-type.md +++ b/docs/stackit_server_machine-type.md @@ -21,10 +21,10 @@ stackit server machine-type [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_machine-type_describe.md b/docs/stackit_server_machine-type_describe.md index c79ec2a84..a35a9b8fe 100644 --- a/docs/stackit_server_machine-type_describe.md +++ b/docs/stackit_server_machine-type_describe.md @@ -31,10 +31,10 @@ stackit server machine-type describe MACHINE_TYPE [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_machine-type_list.md b/docs/stackit_server_machine-type_list.md index fd2ed7afe..405f8c365 100644 --- a/docs/stackit_server_machine-type_list.md +++ b/docs/stackit_server_machine-type_list.md @@ -21,13 +21,20 @@ stackit server machine-type list [flags] List the first 10 machine types $ stackit server machine-type list --limit=10 + + List machine types with exactly 2 vCPUs + $ stackit server machine-type list --filter="vcpus==2" + + List machine types with at least 2 vCPUs and 2048 MB RAM + $ stackit server machine-type list --filter="vcpus >= 2 && ram >= 2048" ``` ### Options ``` - -h, --help Help for "stackit server machine-type list" - --limit int Limit the output to the first n elements + --filter string Filter resources by fields. A subset of expr-lang is supported. See https://expr-lang.org/docs/language-definition for usage details + -h, --help Help for "stackit server machine-type list" + --limit int Limit the output to the first n elements ``` ### Options inherited from parent commands @@ -35,10 +42,10 @@ stackit server machine-type list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_network-interface.md b/docs/stackit_server_network-interface.md index c198fb69f..742f7cb0f 100644 --- a/docs/stackit_server_network-interface.md +++ b/docs/stackit_server_network-interface.md @@ -21,10 +21,10 @@ stackit server network-interface [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_network-interface_attach.md b/docs/stackit_server_network-interface_attach.md index f20e49cb5..3ad6fa2bd 100644 --- a/docs/stackit_server_network-interface_attach.md +++ b/docs/stackit_server_network-interface_attach.md @@ -35,10 +35,10 @@ stackit server network-interface attach [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_network-interface_detach.md b/docs/stackit_server_network-interface_detach.md index 19369455a..fa5c2fbf8 100644 --- a/docs/stackit_server_network-interface_detach.md +++ b/docs/stackit_server_network-interface_detach.md @@ -35,10 +35,10 @@ stackit server network-interface detach [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_network-interface_list.md b/docs/stackit_server_network-interface_list.md index 42ed2e5b5..c458511bd 100644 --- a/docs/stackit_server_network-interface_list.md +++ b/docs/stackit_server_network-interface_list.md @@ -36,10 +36,10 @@ stackit server network-interface list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update.md b/docs/stackit_server_os-update.md index baf0ad8cc..581190fc8 100644 --- a/docs/stackit_server_os-update.md +++ b/docs/stackit_server_os-update.md @@ -21,10 +21,10 @@ stackit server os-update [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_create.md b/docs/stackit_server_os-update_create.md index 4d110f8bc..b6ef789a6 100644 --- a/docs/stackit_server_os-update_create.md +++ b/docs/stackit_server_os-update_create.md @@ -23,9 +23,9 @@ stackit server os-update create [flags] ### Options ``` - -h, --help Help for "stackit server os-update create" - -m, --maintenance-window int Maintenance window (in hours, 1-24) (default 23) - -s, --server-id string Server ID + -h, --help Help for "stackit server os-update create" + -m, --maintenance-window int32 Maintenance window (in hours, 1-24) (default 23) + -s, --server-id string Server ID ``` ### Options inherited from parent commands @@ -33,10 +33,10 @@ stackit server os-update create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_describe.md b/docs/stackit_server_os-update_describe.md index 8302a131f..847aeb41a 100644 --- a/docs/stackit_server_os-update_describe.md +++ b/docs/stackit_server_os-update_describe.md @@ -32,10 +32,10 @@ stackit server os-update describe UPDATE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_disable.md b/docs/stackit_server_os-update_disable.md index 5be186b0b..be2dee1f5 100644 --- a/docs/stackit_server_os-update_disable.md +++ b/docs/stackit_server_os-update_disable.md @@ -29,10 +29,10 @@ stackit server os-update disable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_enable.md b/docs/stackit_server_os-update_enable.md index fdcc98abe..a702fe39b 100644 --- a/docs/stackit_server_os-update_enable.md +++ b/docs/stackit_server_os-update_enable.md @@ -29,10 +29,10 @@ stackit server os-update enable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_list.md b/docs/stackit_server_os-update_list.md index 97ff3bad7..fa2007ac6 100644 --- a/docs/stackit_server_os-update_list.md +++ b/docs/stackit_server_os-update_list.md @@ -33,10 +33,10 @@ stackit server os-update list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_schedule.md b/docs/stackit_server_os-update_schedule.md index 1cc934797..4a174f00f 100644 --- a/docs/stackit_server_os-update_schedule.md +++ b/docs/stackit_server_os-update_schedule.md @@ -21,10 +21,10 @@ stackit server os-update schedule [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_schedule_create.md b/docs/stackit_server_os-update_schedule_create.md index 75862ef17..8ca37455e 100644 --- a/docs/stackit_server_os-update_schedule_create.md +++ b/docs/stackit_server_os-update_schedule_create.md @@ -23,12 +23,12 @@ stackit server os-update schedule create [flags] ### Options ``` - -e, --enabled Is the server os-update schedule enabled (default true) - -h, --help Help for "stackit server os-update schedule create" - -d, --maintenance-window int os-update maintenance window (in hours, 1-24) (default 23) - -n, --name string os-update schedule name - -r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") - -s, --server-id string Server ID + -e, --enabled Is the server os-update schedule enabled (default true) + -h, --help Help for "stackit server os-update schedule create" + -d, --maintenance-window int32 os-update maintenance window (in hours, 1-24) (default 23) + -n, --name string os-update schedule name + -r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") + -s, --server-id string Server ID ``` ### Options inherited from parent commands @@ -36,10 +36,10 @@ stackit server os-update schedule create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_schedule_delete.md b/docs/stackit_server_os-update_schedule_delete.md index c61c8b7ce..a320b1b73 100644 --- a/docs/stackit_server_os-update_schedule_delete.md +++ b/docs/stackit_server_os-update_schedule_delete.md @@ -29,10 +29,10 @@ stackit server os-update schedule delete SCHEDULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_schedule_describe.md b/docs/stackit_server_os-update_schedule_describe.md index f93d219ac..c705d3da2 100644 --- a/docs/stackit_server_os-update_schedule_describe.md +++ b/docs/stackit_server_os-update_schedule_describe.md @@ -32,10 +32,10 @@ stackit server os-update schedule describe SCHEDULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_schedule_list.md b/docs/stackit_server_os-update_schedule_list.md index 3cf2d1580..e1acca16c 100644 --- a/docs/stackit_server_os-update_schedule_list.md +++ b/docs/stackit_server_os-update_schedule_list.md @@ -33,10 +33,10 @@ stackit server os-update schedule list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_os-update_schedule_update.md b/docs/stackit_server_os-update_schedule_update.md index 8a29cd366..607150636 100644 --- a/docs/stackit_server_os-update_schedule_update.md +++ b/docs/stackit_server_os-update_schedule_update.md @@ -20,12 +20,12 @@ stackit server os-update schedule update SCHEDULE_ID [flags] ### Options ``` - -e, --enabled Is the server os-update schedule enabled (default true) - -h, --help Help for "stackit server os-update schedule update" - -d, --maintenance-window int Maintenance window (in hours, 1-24) (default 23) - -n, --name string os-update schedule name - -r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") - -s, --server-id string Server ID + -e, --enabled Is the server os-update schedule enabled (default true) + -h, --help Help for "stackit server os-update schedule update" + -d, --maintenance-window int32 Maintenance window (in hours, 1-24) (default 23) + -n, --name string os-update schedule name + -r, --rrule string os-update RRULE (recurrence rule) (default "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1") + -s, --server-id string Server ID ``` ### Options inherited from parent commands @@ -33,10 +33,10 @@ stackit server os-update schedule update SCHEDULE_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_public-ip.md b/docs/stackit_server_public-ip.md index 6ad5bfc99..98f26b955 100644 --- a/docs/stackit_server_public-ip.md +++ b/docs/stackit_server_public-ip.md @@ -21,10 +21,10 @@ stackit server public-ip [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_public-ip_attach.md b/docs/stackit_server_public-ip_attach.md index a3cc5172f..e7299a2a5 100644 --- a/docs/stackit_server_public-ip_attach.md +++ b/docs/stackit_server_public-ip_attach.md @@ -29,10 +29,10 @@ stackit server public-ip attach PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_public-ip_detach.md b/docs/stackit_server_public-ip_detach.md index 4881e3c2d..ac30baea9 100644 --- a/docs/stackit_server_public-ip_detach.md +++ b/docs/stackit_server_public-ip_detach.md @@ -29,10 +29,10 @@ stackit server public-ip detach PUBLIC_IP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_reboot.md b/docs/stackit_server_reboot.md index 8075a67ba..1353ae9d0 100644 --- a/docs/stackit_server_reboot.md +++ b/docs/stackit_server_reboot.md @@ -32,10 +32,10 @@ stackit server reboot SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_rescue.md b/docs/stackit_server_rescue.md index 4aaa5104b..a42ad285e 100644 --- a/docs/stackit_server_rescue.md +++ b/docs/stackit_server_rescue.md @@ -29,10 +29,10 @@ stackit server rescue SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_resize.md b/docs/stackit_server_resize.md index c91335432..3c70bcb7e 100644 --- a/docs/stackit_server_resize.md +++ b/docs/stackit_server_resize.md @@ -21,7 +21,7 @@ stackit server resize SERVER_ID [flags] ``` -h, --help Help for "stackit server resize" - --machine-type string Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html + --machine-type string Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/ ``` ### Options inherited from parent commands @@ -29,10 +29,10 @@ stackit server resize SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_security-group.md b/docs/stackit_server_security-group.md new file mode 100644 index 000000000..4371f853d --- /dev/null +++ b/docs/stackit_server_security-group.md @@ -0,0 +1,35 @@ +## stackit server security-group + +Allows attaching/detaching security groups to servers + +### Synopsis + +Allows attaching/detaching security groups to servers. + +``` +stackit server security-group [flags] +``` + +### Options + +``` + -h, --help Help for "stackit server security-group" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit server](./stackit_server.md) - Provides functionality for servers +* [stackit server security-group attach](./stackit_server_security-group_attach.md) - Attaches a security group to a server +* [stackit server security-group detach](./stackit_server_security-group_detach.md) - Detaches a security group from a server + diff --git a/docs/stackit_server_security-group_attach.md b/docs/stackit_server_security-group_attach.md new file mode 100644 index 000000000..8f60a4c1e --- /dev/null +++ b/docs/stackit_server_security-group_attach.md @@ -0,0 +1,42 @@ +## stackit server security-group attach + +Attaches a security group to a server + +### Synopsis + +Attaches a security group to a server. + +``` +stackit server security-group attach [flags] +``` + +### Examples + +``` + Attach a security group with ID "xxx" to a server with ID "yyy" + $ stackit server security-group attach --server-id yyy --security-group-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit server security-group attach" + --security-group-id string Security Group ID + --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit server security-group](./stackit_server_security-group.md) - Allows attaching/detaching security groups to servers + diff --git a/docs/stackit_server_security-group_detach.md b/docs/stackit_server_security-group_detach.md new file mode 100644 index 000000000..c0719430d --- /dev/null +++ b/docs/stackit_server_security-group_detach.md @@ -0,0 +1,42 @@ +## stackit server security-group detach + +Detaches a security group from a server + +### Synopsis + +Detaches a security group from a server. + +``` +stackit server security-group detach [flags] +``` + +### Examples + +``` + Detach a security group with ID "xxx" from a server with ID "yyy" + $ stackit server security-group detach --server-id yyy --security-group-id xxx +``` + +### Options + +``` + -h, --help Help for "stackit server security-group detach" + --security-group-id string Security Group ID + --server-id string Server ID +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit server security-group](./stackit_server_security-group.md) - Allows attaching/detaching security groups to servers + diff --git a/docs/stackit_server_service-account.md b/docs/stackit_server_service-account.md index 5af599a53..375145c72 100644 --- a/docs/stackit_server_service-account.md +++ b/docs/stackit_server_service-account.md @@ -21,10 +21,10 @@ stackit server service-account [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_service-account_attach.md b/docs/stackit_server_service-account_attach.md index 0cf08c386..26df44ffd 100644 --- a/docs/stackit_server_service-account_attach.md +++ b/docs/stackit_server_service-account_attach.md @@ -7,21 +7,22 @@ Attach a service account to a server Attach a service account to a server ``` -stackit server service-account attach SERVICE_ACCOUNT_EMAIL [flags] +stackit server service-account attach [flags] ``` ### Examples ``` Attach a service account with mail "xxx@sa.stackit.cloud" to a server with ID "yyy" - $ stackit server service-account attach xxx@sa.stackit.cloud --server-id yyy + $ stackit server service-account attach --service-account-email xxx@sa.stackit.cloud --server-id yyy ``` ### Options ``` - -h, --help Help for "stackit server service-account attach" - -s, --server-id string Server ID + -h, --help Help for "stackit server service-account attach" + -s, --server-id string Server ID + -a, --service-account-email string Service Account Email ``` ### Options inherited from parent commands @@ -29,10 +30,10 @@ stackit server service-account attach SERVICE_ACCOUNT_EMAIL [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_service-account_detach.md b/docs/stackit_server_service-account_detach.md index 87806ced3..b409d406b 100644 --- a/docs/stackit_server_service-account_detach.md +++ b/docs/stackit_server_service-account_detach.md @@ -7,21 +7,22 @@ Detach a service account from a server Detach a service account from a server ``` -stackit server service-account detach SERVICE_ACCOUNT_EMAIL [flags] +stackit server service-account detach [flags] ``` ### Examples ``` Detach a service account with mail "xxx@sa.stackit.cloud" from a server "yyy" - $ stackit server service-account detach xxx@sa.stackit.cloud --server-id yyy + $ stackit server service-account detach --service-account-email xxx@sa.stackit.cloud --server-id yyy ``` ### Options ``` - -h, --help Help for "stackit server service-account detach" - -s, --server-id string Server id + -h, --help Help for "stackit server service-account detach" + -s, --server-id string Server id + -a, --service-account-email string Service Account Email ``` ### Options inherited from parent commands @@ -29,10 +30,10 @@ stackit server service-account detach SERVICE_ACCOUNT_EMAIL [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_service-account_list.md b/docs/stackit_server_service-account_list.md index 78349aee3..53a629223 100644 --- a/docs/stackit_server_service-account_list.md +++ b/docs/stackit_server_service-account_list.md @@ -36,10 +36,10 @@ stackit server service-account list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_start.md b/docs/stackit_server_start.md index 1fa89116d..21ef308a5 100644 --- a/docs/stackit_server_start.md +++ b/docs/stackit_server_start.md @@ -28,10 +28,10 @@ stackit server start SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_stop.md b/docs/stackit_server_stop.md index 41403e1f7..44e501a67 100644 --- a/docs/stackit_server_stop.md +++ b/docs/stackit_server_stop.md @@ -28,10 +28,10 @@ stackit server stop SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_unrescue.md b/docs/stackit_server_unrescue.md index 5dc30bab3..0c111d8ac 100644 --- a/docs/stackit_server_unrescue.md +++ b/docs/stackit_server_unrescue.md @@ -28,10 +28,10 @@ stackit server unrescue SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_update.md b/docs/stackit_server_update.md index 3aac20259..c47e8810a 100644 --- a/docs/stackit_server_update.md +++ b/docs/stackit_server_update.md @@ -33,10 +33,10 @@ stackit server update SERVER_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_volume.md b/docs/stackit_server_volume.md index 74e426604..da58fe5e7 100644 --- a/docs/stackit_server_volume.md +++ b/docs/stackit_server_volume.md @@ -21,10 +21,10 @@ stackit server volume [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_volume_attach.md b/docs/stackit_server_volume_attach.md index b7014c6a1..15de47087 100644 --- a/docs/stackit_server_volume_attach.md +++ b/docs/stackit_server_volume_attach.md @@ -33,10 +33,10 @@ stackit server volume attach VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_volume_describe.md b/docs/stackit_server_volume_describe.md index be85d6afc..8a399fbe8 100644 --- a/docs/stackit_server_volume_describe.md +++ b/docs/stackit_server_volume_describe.md @@ -35,10 +35,10 @@ stackit server volume describe VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_volume_detach.md b/docs/stackit_server_volume_detach.md index 3758a7b35..0106a4a1b 100644 --- a/docs/stackit_server_volume_detach.md +++ b/docs/stackit_server_volume_detach.md @@ -29,10 +29,10 @@ stackit server volume detach VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_volume_list.md b/docs/stackit_server_volume_list.md index 97560e090..06be69594 100644 --- a/docs/stackit_server_volume_list.md +++ b/docs/stackit_server_volume_list.md @@ -32,10 +32,10 @@ stackit server volume list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_server_volume_update.md b/docs/stackit_server_volume_update.md index 70290e948..aabe54f13 100644 --- a/docs/stackit_server_volume_update.md +++ b/docs/stackit_server_volume_update.md @@ -30,10 +30,10 @@ stackit server volume update VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account.md b/docs/stackit_service-account.md index fbd524334..2d5d4d37d 100644 --- a/docs/stackit_service-account.md +++ b/docs/stackit_service-account.md @@ -21,10 +21,10 @@ stackit service-account [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_create.md b/docs/stackit_service-account_create.md index cb1617fc7..d28c26f6d 100644 --- a/docs/stackit_service-account_create.md +++ b/docs/stackit_service-account_create.md @@ -29,10 +29,10 @@ stackit service-account create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_delete.md b/docs/stackit_service-account_delete.md index 3e011d03f..e466da9ef 100644 --- a/docs/stackit_service-account_delete.md +++ b/docs/stackit_service-account_delete.md @@ -28,10 +28,10 @@ stackit service-account delete EMAIL [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_get-jwks.md b/docs/stackit_service-account_get-jwks.md index 2c34e76fb..9cd0a2519 100644 --- a/docs/stackit_service-account_get-jwks.md +++ b/docs/stackit_service-account_get-jwks.md @@ -28,10 +28,10 @@ stackit service-account get-jwks EMAIL [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_key.md b/docs/stackit_service-account_key.md index bb4306f77..bbda852da 100644 --- a/docs/stackit_service-account_key.md +++ b/docs/stackit_service-account_key.md @@ -21,10 +21,10 @@ stackit service-account key [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_key_create.md b/docs/stackit_service-account_key_create.md index 0ebcf7d02..108638ccb 100644 --- a/docs/stackit_service-account_key_create.md +++ b/docs/stackit_service-account_key_create.md @@ -39,10 +39,10 @@ stackit service-account key create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_key_delete.md b/docs/stackit_service-account_key_delete.md index b039f82f4..7d4b60b8e 100644 --- a/docs/stackit_service-account_key_delete.md +++ b/docs/stackit_service-account_key_delete.md @@ -29,10 +29,10 @@ stackit service-account key delete KEY_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_key_describe.md b/docs/stackit_service-account_key_describe.md index 2ff0ac156..32212c019 100644 --- a/docs/stackit_service-account_key_describe.md +++ b/docs/stackit_service-account_key_describe.md @@ -29,10 +29,10 @@ stackit service-account key describe KEY_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_key_list.md b/docs/stackit_service-account_key_list.md index 8ad4ad291..6d65b39f5 100644 --- a/docs/stackit_service-account_key_list.md +++ b/docs/stackit_service-account_key_list.md @@ -36,10 +36,10 @@ stackit service-account key list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_key_update.md b/docs/stackit_service-account_key_update.md index f16648405..c2eb660c3 100644 --- a/docs/stackit_service-account_key_update.md +++ b/docs/stackit_service-account_key_update.md @@ -39,10 +39,10 @@ stackit service-account key update KEY_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_list.md b/docs/stackit_service-account_list.md index 6e00f821c..421074710 100644 --- a/docs/stackit_service-account_list.md +++ b/docs/stackit_service-account_list.md @@ -29,10 +29,10 @@ stackit service-account list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_token.md b/docs/stackit_service-account_token.md index d417d9095..fcf521ca1 100644 --- a/docs/stackit_service-account_token.md +++ b/docs/stackit_service-account_token.md @@ -21,10 +21,10 @@ stackit service-account token [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_token_create.md b/docs/stackit_service-account_token_create.md index bcdc6fc41..e4e299392 100644 --- a/docs/stackit_service-account_token_create.md +++ b/docs/stackit_service-account_token_create.md @@ -35,10 +35,10 @@ stackit service-account token create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_token_list.md b/docs/stackit_service-account_token_list.md index 800689e4c..46cbb4c18 100644 --- a/docs/stackit_service-account_token_list.md +++ b/docs/stackit_service-account_token_list.md @@ -38,10 +38,10 @@ stackit service-account token list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_service-account_token_revoke.md b/docs/stackit_service-account_token_revoke.md index d295e8168..a226c1c53 100644 --- a/docs/stackit_service-account_token_revoke.md +++ b/docs/stackit_service-account_token_revoke.md @@ -31,10 +31,10 @@ stackit service-account token revoke TOKEN_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske.md b/docs/stackit_ske.md index b6a307937..dfcc13116 100644 --- a/docs/stackit_ske.md +++ b/docs/stackit_ske.md @@ -21,10 +21,10 @@ stackit ske [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster.md b/docs/stackit_ske_cluster.md index a575e5495..6ecab2783 100644 --- a/docs/stackit_ske_cluster.md +++ b/docs/stackit_ske_cluster.md @@ -21,10 +21,10 @@ stackit ske cluster [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_create.md b/docs/stackit_ske_cluster_create.md index 3c94a7bdd..8dc73c36d 100644 --- a/docs/stackit_ske_cluster_create.md +++ b/docs/stackit_ske_cluster_create.md @@ -42,10 +42,10 @@ stackit ske cluster create CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_delete.md b/docs/stackit_ske_cluster_delete.md index c1c0407a7..0c27d8a88 100644 --- a/docs/stackit_ske_cluster_delete.md +++ b/docs/stackit_ske_cluster_delete.md @@ -28,10 +28,10 @@ stackit ske cluster delete CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_describe.md b/docs/stackit_ske_cluster_describe.md index 91b3949fc..d934e0078 100644 --- a/docs/stackit_ske_cluster_describe.md +++ b/docs/stackit_ske_cluster_describe.md @@ -31,10 +31,10 @@ stackit ske cluster describe CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_generate-payload.md b/docs/stackit_ske_cluster_generate-payload.md index d5592293f..db49e0480 100644 --- a/docs/stackit_ske_cluster_generate-payload.md +++ b/docs/stackit_ske_cluster_generate-payload.md @@ -41,10 +41,10 @@ stackit ske cluster generate-payload [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_hibernate.md b/docs/stackit_ske_cluster_hibernate.md index 20baddd1b..e626e944e 100644 --- a/docs/stackit_ske_cluster_hibernate.md +++ b/docs/stackit_ske_cluster_hibernate.md @@ -28,10 +28,10 @@ stackit ske cluster hibernate CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_list.md b/docs/stackit_ske_cluster_list.md index a757d19b3..6579158fd 100644 --- a/docs/stackit_ske_cluster_list.md +++ b/docs/stackit_ske_cluster_list.md @@ -35,10 +35,10 @@ stackit ske cluster list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_maintenance.md b/docs/stackit_ske_cluster_maintenance.md index 0a6c6540c..2bc324d1d 100644 --- a/docs/stackit_ske_cluster_maintenance.md +++ b/docs/stackit_ske_cluster_maintenance.md @@ -28,10 +28,10 @@ stackit ske cluster maintenance CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_reconcile.md b/docs/stackit_ske_cluster_reconcile.md index 64887316d..df5fc87f3 100644 --- a/docs/stackit_ske_cluster_reconcile.md +++ b/docs/stackit_ske_cluster_reconcile.md @@ -28,10 +28,10 @@ stackit ske cluster reconcile CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_update.md b/docs/stackit_ske_cluster_update.md index 24fa95748..28289423a 100644 --- a/docs/stackit_ske_cluster_update.md +++ b/docs/stackit_ske_cluster_update.md @@ -39,10 +39,10 @@ stackit ske cluster update CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_cluster_wakeup.md b/docs/stackit_ske_cluster_wakeup.md index 7b07e9965..f639657f0 100644 --- a/docs/stackit_ske_cluster_wakeup.md +++ b/docs/stackit_ske_cluster_wakeup.md @@ -28,10 +28,10 @@ stackit ske cluster wakeup CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_credentials.md b/docs/stackit_ske_credentials.md index 252b629e0..d5de8a89e 100644 --- a/docs/stackit_ske_credentials.md +++ b/docs/stackit_ske_credentials.md @@ -21,10 +21,10 @@ stackit ske credentials [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_credentials_complete-rotation.md b/docs/stackit_ske_credentials_complete-rotation.md index 12536dba5..29db45813 100644 --- a/docs/stackit_ske_credentials_complete-rotation.md +++ b/docs/stackit_ske_credentials_complete-rotation.md @@ -14,7 +14,7 @@ To ensure continued access to the Kubernetes cluster, please update your kubecon If you haven't, please start the process by running: $ stackit ske credentials start-rotation my-cluster -For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html +For more information, visit: https://docs.stackit.cloud/products/runtime/kubernetes-engine/how-tos/rotate-ske-credentials/ ``` stackit ske credentials complete-rotation CLUSTER_NAME [flags] @@ -43,10 +43,10 @@ stackit ske credentials complete-rotation CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_credentials_start-rotation.md b/docs/stackit_ske_credentials_start-rotation.md index aa8160adf..a99123344 100644 --- a/docs/stackit_ske_credentials_start-rotation.md +++ b/docs/stackit_ske_credentials_start-rotation.md @@ -18,7 +18,7 @@ After completing the rotation of credentials, you can generate a new kubeconfig $ stackit ske kubeconfig create my-cluster Complete the rotation by running: $ stackit ske credentials complete-rotation my-cluster -For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html +For more information, visit: https://docs.stackit.cloud/products/runtime/kubernetes-engine/how-tos/rotate-ske-credentials/ ``` stackit ske credentials start-rotation CLUSTER_NAME [flags] @@ -47,10 +47,10 @@ stackit ske credentials start-rotation CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_describe.md b/docs/stackit_ske_describe.md index 1cd29f31d..1764bd6ba 100644 --- a/docs/stackit_ske_describe.md +++ b/docs/stackit_ske_describe.md @@ -28,10 +28,10 @@ stackit ske describe [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_disable.md b/docs/stackit_ske_disable.md index c86d2b10c..f72e3a12d 100644 --- a/docs/stackit_ske_disable.md +++ b/docs/stackit_ske_disable.md @@ -28,10 +28,10 @@ stackit ske disable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_enable.md b/docs/stackit_ske_enable.md index ccc25bebb..2bef68fee 100644 --- a/docs/stackit_ske_enable.md +++ b/docs/stackit_ske_enable.md @@ -28,10 +28,10 @@ stackit ske enable [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_kubeconfig.md b/docs/stackit_ske_kubeconfig.md index 83634e149..84056db65 100644 --- a/docs/stackit_ske_kubeconfig.md +++ b/docs/stackit_ske_kubeconfig.md @@ -21,10 +21,10 @@ stackit ske kubeconfig [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_kubeconfig_create.md b/docs/stackit_ske_kubeconfig_create.md index 476d50bab..e93d7c3cc 100644 --- a/docs/stackit_ske_kubeconfig_create.md +++ b/docs/stackit_ske_kubeconfig_create.md @@ -4,9 +4,9 @@ Creates or update a kubeconfig for a SKE cluster ### Synopsis -Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster, if the config exists in the kubeconfig file the information will be updated. +Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster. By default an admin kubeconfig is created. Use the `--idp` option to create an IDP kubeconfig that authenticates via the STACKIT IDP. -By default, the kubeconfig information of the SKE cluster is merged into the default kubeconfig file of the current user. If the kubeconfig file doesn't exist, a new one will be created. +If the config exists in the kubeconfig file the information will be updated. By default, the kubeconfig information of the SKE cluster is merged into the default kubeconfig file of the current user. If the kubeconfig file doesn't exist, a new one will be created. You can override this behavior by specifying a custom filepath using the --filepath flag or by setting the KUBECONFIG env variable (fallback). An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h. @@ -20,36 +20,40 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] ### Examples ``` - Create or update a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated." - $ stackit ske kubeconfig create my-cluster - - Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. + Get a short-lived admin kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid admin credentials via the `stackit ske kubeconfig login` command. $ stackit ske kubeconfig create my-cluster --login - Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated. + Get an IDP kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not grant permissions in the cluster by default and obtains credentials on-demand via the `stackit ske kubeconfig login` command. + $ stackit ske kubeconfig create my-cluster --idp + + Create or update a short-lived admin kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath. If the config exits in the kubeconfig file the information will be updated. + $ stackit ske kubeconfig create my-cluster --login --filepath /path/to/config + + Create or update an admin kubeconfig for the SKE cluster with name "my-cluster". If the config exits in the kubeconfig file the information will be updated." + $ stackit ske kubeconfig create my-cluster + + Create an admin kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated. $ stackit ske kubeconfig create my-cluster --expiration 30d - Create or update a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated. + Create or update an admin kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated. $ stackit ske kubeconfig create my-cluster --expiration 2M - Create or update a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath. If the config exits in the kubeconfig file the information will be updated. - $ stackit ske kubeconfig create my-cluster --filepath /path/to/config - - Get a kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json + Get an admin kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json $ stackit ske kubeconfig create my-cluster --disable-writing --output-format json - Create a kubeconfig for the SKE cluster with name "my-cluster. It will OVERWRITE your current kubeconfig file." + Create an admin kubeconfig for the SKE cluster with name "my-cluster". It will OVERWRITE your current kubeconfig file. $ stackit ske kubeconfig create my-cluster --overwrite true ``` ### Options ``` - --disable-writing Disable the writing of kubeconfig. Set the output format to json or yaml using the --output-format flag to display the kubeconfig. + --disable-writing Disable the writing of kubeconfig. Set the output format to json or yaml using the -- flag to display the kubeconfig. -e, --expiration string Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h --filepath string Path to create the kubeconfig file. Will fall back to KUBECONFIG env variable if not set. In case both aren't set, the kubeconfig is created as file named 'config' in the .kube folder in the user's home directory. -h, --help Help for "stackit ske kubeconfig create" - -l, --login Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag. + --idp Create a non-admin kubeconfig that uses the STACKIT IDP to obtain credentials. + -l, --login Create a short-lived admin kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag. --overwrite Overwrite the kubeconfig file. ``` @@ -58,10 +62,10 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_kubeconfig_login.md b/docs/stackit_ske_kubeconfig_login.md index 0b5441533..0df7567e5 100644 --- a/docs/stackit_ske_kubeconfig_login.md +++ b/docs/stackit_ske_kubeconfig_login.md @@ -5,8 +5,8 @@ Login plugin for kubernetes clients ### Synopsis Login plugin for kubernetes clients, that creates short-lived credentials to authenticate against a STACKIT Kubernetes Engine (SKE) cluster. -First you need to obtain a kubeconfig for use with the login command (first example). -Secondly you use the kubeconfig with your chosen Kubernetes client (second example), the client will automatically retrieve the credentials via the STACKIT CLI. +First you need to obtain a kubeconfig for use with the login command (first or second example). +Secondly you use the kubeconfig with your chosen Kubernetes client (third example), the client will automatically retrieve the credentials via the STACKIT CLI. ``` stackit ske kubeconfig login [flags] @@ -15,9 +15,12 @@ stackit ske kubeconfig login [flags] ### Examples ``` - Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. + Get an admin, login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid admin credentials via the `stackit ske kubeconfig login` command. $ stackit ske kubeconfig create my-cluster --login + Get an IDP kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. + $ stackit ske kubeconfig create my-cluster --idp + Use the previously saved kubeconfig to authenticate to the SKE cluster, in this case with kubectl. $ kubectl cluster-info $ kubectl get pods @@ -27,6 +30,7 @@ stackit ske kubeconfig login [flags] ``` -h, --help Help for "stackit ske kubeconfig login" + --idp Use the STACKIT IdP for authentication to the cluster. ``` ### Options inherited from parent commands @@ -34,10 +38,10 @@ stackit ske kubeconfig login [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_ske_options.md b/docs/stackit_ske_options.md index 76afbe93c..8dbcddd46 100644 --- a/docs/stackit_ske_options.md +++ b/docs/stackit_ske_options.md @@ -4,6 +4,7 @@ Lists SKE provider options ### Synopsis +Command "options" is deprecated, use the subcommands instead. Lists STACKIT Kubernetes Engine (SKE) provider options (availability zones, Kubernetes versions, machine images and types, volume types). Pass one or more flags to filter what categories are shown. @@ -11,28 +12,10 @@ Pass one or more flags to filter what categories are shown. stackit ske options [flags] ``` -### Examples - -``` - List SKE options for all categories - $ stackit ske options - - List SKE options regarding Kubernetes versions only - $ stackit ske options --kubernetes-versions - - List SKE options regarding Kubernetes versions and machine images - $ stackit ske options --kubernetes-versions --machine-images -``` - ### Options ``` - --availability-zones Lists availability zones - -h, --help Help for "stackit ske options" - --kubernetes-versions Lists supported kubernetes versions - --machine-images Lists supported machine images - --machine-types Lists supported machine types - --volume-types Lists supported volume types + -h, --help Help for "stackit ske options" ``` ### Options inherited from parent commands @@ -40,13 +23,18 @@ stackit ske options [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO * [stackit ske](./stackit_ske.md) - Provides functionality for SKE +* [stackit ske options availability-zones](./stackit_ske_options_availability-zones.md) - Lists SKE provider options for availability-zones +* [stackit ske options kubernetes-versions](./stackit_ske_options_kubernetes-versions.md) - Lists SKE provider options for kubernetes-versions +* [stackit ske options machine-images](./stackit_ske_options_machine-images.md) - Lists SKE provider options for machine-images +* [stackit ske options machine-types](./stackit_ske_options_machine-types.md) - Lists SKE provider options for machine-types +* [stackit ske options volume-types](./stackit_ske_options_volume-types.md) - Lists SKE provider options for volume-types diff --git a/docs/stackit_ske_options_availability-zones.md b/docs/stackit_ske_options_availability-zones.md new file mode 100644 index 000000000..77ecaf24e --- /dev/null +++ b/docs/stackit_ske_options_availability-zones.md @@ -0,0 +1,40 @@ +## stackit ske options availability-zones + +Lists SKE provider options for availability-zones + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for availability-zones. + +``` +stackit ske options availability-zones [flags] +``` + +### Examples + +``` + List SKE options for availability-zones + $ stackit ske options availability-zones +``` + +### Options + +``` + -h, --help Help for "stackit ske options availability-zones" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_kubernetes-versions.md b/docs/stackit_ske_options_kubernetes-versions.md new file mode 100644 index 000000000..3d0b25d58 --- /dev/null +++ b/docs/stackit_ske_options_kubernetes-versions.md @@ -0,0 +1,44 @@ +## stackit ske options kubernetes-versions + +Lists SKE provider options for kubernetes-versions + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for kubernetes-versions. + +``` +stackit ske options kubernetes-versions [flags] +``` + +### Examples + +``` + List SKE options for kubernetes-versions + $ stackit ske options kubernetes-versions + + List SKE options for supported kubernetes-versions + $ stackit ske options kubernetes-versions --supported +``` + +### Options + +``` + -h, --help Help for "stackit ske options kubernetes-versions" + --supported List supported versions only +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_machine-images.md b/docs/stackit_ske_options_machine-images.md new file mode 100644 index 000000000..646c4fc31 --- /dev/null +++ b/docs/stackit_ske_options_machine-images.md @@ -0,0 +1,40 @@ +## stackit ske options machine-images + +Lists SKE provider options for machine-images + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for machine-images. + +``` +stackit ske options machine-images [flags] +``` + +### Examples + +``` + List SKE options for machine-images + $ stackit ske options machine-images +``` + +### Options + +``` + -h, --help Help for "stackit ske options machine-images" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_machine-types.md b/docs/stackit_ske_options_machine-types.md new file mode 100644 index 000000000..d5601ed3e --- /dev/null +++ b/docs/stackit_ske_options_machine-types.md @@ -0,0 +1,40 @@ +## stackit ske options machine-types + +Lists SKE provider options for machine-types + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for machine-types. + +``` +stackit ske options machine-types [flags] +``` + +### Examples + +``` + List SKE options for machine-types + $ stackit ske options machine-types +``` + +### Options + +``` + -h, --help Help for "stackit ske options machine-types" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_volume-types.md b/docs/stackit_ske_options_volume-types.md new file mode 100644 index 000000000..54f032aa8 --- /dev/null +++ b/docs/stackit_ske_options_volume-types.md @@ -0,0 +1,40 @@ +## stackit ske options volume-types + +Lists SKE provider options for volume-types + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for volume-types. + +``` +stackit ske options volume-types [flags] +``` + +### Examples + +``` + List SKE options for volume-types + $ stackit ske options volume-types +``` + +### Options + +``` + -h, --help Help for "stackit ske options volume-types" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_volume.md b/docs/stackit_volume.md index 3412504c2..67664d6ba 100644 --- a/docs/stackit_volume.md +++ b/docs/stackit_volume.md @@ -21,10 +21,10 @@ stackit volume [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup.md b/docs/stackit_volume_backup.md index f6390f385..23d579a9c 100644 --- a/docs/stackit_volume_backup.md +++ b/docs/stackit_volume_backup.md @@ -21,10 +21,10 @@ stackit volume backup [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup_create.md b/docs/stackit_volume_backup_create.md index 5a322f34a..22fcd307f 100644 --- a/docs/stackit_volume_backup_create.md +++ b/docs/stackit_volume_backup_create.md @@ -30,7 +30,7 @@ stackit volume backup create [flags] --labels stringToString Key-value string pairs as labels (default []) --name string Name of the backup --source-id string ID of the source from which a backup should be created - --source-type string Source type of the backup, one of ["volume" "snapshot"] + --source-type string Source type of the backup, (one of: [volume, snapshot]) ``` ### Options inherited from parent commands @@ -38,10 +38,10 @@ stackit volume backup create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup_delete.md b/docs/stackit_volume_backup_delete.md index 5300f7854..e0d43a4ce 100644 --- a/docs/stackit_volume_backup_delete.md +++ b/docs/stackit_volume_backup_delete.md @@ -28,10 +28,10 @@ stackit volume backup delete BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup_describe.md b/docs/stackit_volume_backup_describe.md index dbff5e4dc..dd4b3e1d6 100644 --- a/docs/stackit_volume_backup_describe.md +++ b/docs/stackit_volume_backup_describe.md @@ -31,10 +31,10 @@ stackit volume backup describe BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup_list.md b/docs/stackit_volume_backup_list.md index 91f3ca99a..5c06dec33 100644 --- a/docs/stackit_volume_backup_list.md +++ b/docs/stackit_volume_backup_list.md @@ -39,10 +39,10 @@ stackit volume backup list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup_restore.md b/docs/stackit_volume_backup_restore.md index 80dc563db..95e68d9b7 100644 --- a/docs/stackit_volume_backup_restore.md +++ b/docs/stackit_volume_backup_restore.md @@ -28,10 +28,10 @@ stackit volume backup restore BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_backup_update.md b/docs/stackit_volume_backup_update.md index 02f86f4e8..0c08c6b32 100644 --- a/docs/stackit_volume_backup_update.md +++ b/docs/stackit_volume_backup_update.md @@ -33,10 +33,10 @@ stackit volume backup update BACKUP_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_create.md b/docs/stackit_volume_create.md index dedf3c595..d10e8e992 100644 --- a/docs/stackit_volume_create.md +++ b/docs/stackit_volume_create.md @@ -45,10 +45,10 @@ stackit volume create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_delete.md b/docs/stackit_volume_delete.md index 165804aa6..8f7eaadd5 100644 --- a/docs/stackit_volume_delete.md +++ b/docs/stackit_volume_delete.md @@ -30,10 +30,10 @@ stackit volume delete VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_describe.md b/docs/stackit_volume_describe.md index a098db3a7..f8df34be6 100644 --- a/docs/stackit_volume_describe.md +++ b/docs/stackit_volume_describe.md @@ -31,10 +31,10 @@ stackit volume describe VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_list.md b/docs/stackit_volume_list.md index 2e59fd0d7..8e9ea498c 100644 --- a/docs/stackit_volume_list.md +++ b/docs/stackit_volume_list.md @@ -39,10 +39,10 @@ stackit volume list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_performance-class.md b/docs/stackit_volume_performance-class.md index f584910ab..d3c08f0b3 100644 --- a/docs/stackit_volume_performance-class.md +++ b/docs/stackit_volume_performance-class.md @@ -21,10 +21,10 @@ stackit volume performance-class [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_performance-class_describe.md b/docs/stackit_volume_performance-class_describe.md index a7c53a69c..8b7af4b28 100644 --- a/docs/stackit_volume_performance-class_describe.md +++ b/docs/stackit_volume_performance-class_describe.md @@ -31,10 +31,10 @@ stackit volume performance-class describe VOLUME_PERFORMANCE_CLASS [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_performance-class_list.md b/docs/stackit_volume_performance-class_list.md index e01cd3df4..4422a4c84 100644 --- a/docs/stackit_volume_performance-class_list.md +++ b/docs/stackit_volume_performance-class_list.md @@ -39,10 +39,10 @@ stackit volume performance-class list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_resize.md b/docs/stackit_volume_resize.md index 04286ff44..b32203f4c 100644 --- a/docs/stackit_volume_resize.md +++ b/docs/stackit_volume_resize.md @@ -29,10 +29,10 @@ stackit volume resize VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_snapshot.md b/docs/stackit_volume_snapshot.md index 61f6f428e..8728cb9f2 100644 --- a/docs/stackit_volume_snapshot.md +++ b/docs/stackit_volume_snapshot.md @@ -21,10 +21,10 @@ stackit volume snapshot [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_snapshot_create.md b/docs/stackit_volume_snapshot_create.md index 4ed86ad39..9ff3e553f 100644 --- a/docs/stackit_volume_snapshot_create.md +++ b/docs/stackit_volume_snapshot_create.md @@ -37,10 +37,10 @@ stackit volume snapshot create [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_snapshot_delete.md b/docs/stackit_volume_snapshot_delete.md index df9a37828..d9c5a8be5 100644 --- a/docs/stackit_volume_snapshot_delete.md +++ b/docs/stackit_volume_snapshot_delete.md @@ -28,10 +28,10 @@ stackit volume snapshot delete SNAPSHOT_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_snapshot_describe.md b/docs/stackit_volume_snapshot_describe.md index 5f7f256b7..5b030540d 100644 --- a/docs/stackit_volume_snapshot_describe.md +++ b/docs/stackit_volume_snapshot_describe.md @@ -31,10 +31,10 @@ stackit volume snapshot describe SNAPSHOT_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_snapshot_list.md b/docs/stackit_volume_snapshot_list.md index f4fe9dd3a..03a5cd46e 100644 --- a/docs/stackit_volume_snapshot_list.md +++ b/docs/stackit_volume_snapshot_list.md @@ -36,10 +36,10 @@ stackit volume snapshot list [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_snapshot_update.md b/docs/stackit_volume_snapshot_update.md index 2b74b5ae8..9df1cbada 100644 --- a/docs/stackit_volume_snapshot_update.md +++ b/docs/stackit_volume_snapshot_update.md @@ -33,10 +33,10 @@ stackit volume snapshot update SNAPSHOT_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/docs/stackit_volume_update.md b/docs/stackit_volume_update.md index 1f28e3b1e..0d09cedb7 100644 --- a/docs/stackit_volume_update.md +++ b/docs/stackit_volume_update.md @@ -37,10 +37,10 @@ stackit volume update VOLUME_ID [flags] ``` -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously - -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -o, --output-format string Output format, (one of: [json, pretty, none, yaml]) -p, --project-id string Project ID --region string Target region for region-specific requests - --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") + --verbosity string Verbosity of the CLI, (one of: [debug, info, warning, error]) (default "info") ``` ### SEE ALSO diff --git a/go.mod b/go.mod index 125b89639..ebe7634c8 100644 --- a/go.mod +++ b/go.mod @@ -1,100 +1,118 @@ module github.com/stackitcloud/stackit-cli -go 1.24 +go 1.26.0 require ( - github.com/fatih/color v1.18.0 - github.com/goccy/go-yaml v1.18.0 - github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/fatih/color v1.19.0 + github.com/goccy/go-yaml v1.19.2 + github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf - github.com/jedib0t/go-pretty/v6 v6.6.8 - github.com/lmittmann/tint v1.1.2 + github.com/jedib0t/go-pretty/v6 v6.8.2 + github.com/lmittmann/tint v1.1.3 github.com/mattn/go-colorable v0.1.14 - github.com/spf13/cobra v1.9.1 - github.com/spf13/pflag v1.0.7 - github.com/spf13/viper v1.20.1 - github.com/stackitcloud/stackit-sdk-go/core v0.17.3 - github.com/stackitcloud/stackit-sdk-go/services/alb v0.6.1 - github.com/stackitcloud/stackit-sdk-go/services/authorization v0.8.1 - github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 - github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1 - github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.1 - github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 - github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1 - github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 - github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1 - github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 - github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1 - github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 - github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 - github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.0 - github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 - github.com/stackitcloud/stackit-sdk-go/services/ske v1.3.0 - github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.1 - github.com/zalando/go-keyring v0.2.6 - golang.org/x/mod v0.27.0 - golang.org/x/oauth2 v0.30.0 - golang.org/x/term v0.34.0 - golang.org/x/text v0.28.0 - k8s.io/apimachinery v0.32.3 - k8s.io/client-go v0.32.3 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + github.com/stackitcloud/stackit-sdk-go/core v0.26.0 + github.com/stackitcloud/stackit-sdk-go/services/alb v0.14.2 + github.com/stackitcloud/stackit-sdk-go/services/authorization v0.12.0 + github.com/stackitcloud/stackit-sdk-go/services/cdn v1.16.0 + github.com/stackitcloud/stackit-sdk-go/services/dns v0.21.0 + github.com/stackitcloud/stackit-sdk-go/services/edge v0.4.3 + github.com/stackitcloud/stackit-sdk-go/services/git v0.14.0 + github.com/stackitcloud/stackit-sdk-go/services/iaas v1.13.0 + github.com/stackitcloud/stackit-sdk-go/services/intake v0.7.1 + github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0 + github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.0 + github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.1.0 + github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.11.0 + github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0 + github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.8.0 + github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.18.1 + github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.7.0 + github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.5.2 + github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.12.0 + github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0 + github.com/stackitcloud/stackit-sdk-go/services/ske v1.11.0 + github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.11.0 + github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0 + github.com/zalando/go-keyring v0.2.8 + golang.org/x/mod v0.37.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/term v0.44.0 + golang.org/x/text v0.40.0 + k8s.io/apimachinery v0.36.2 + k8s.io/client-go v0.36.2 ) require ( - golang.org/x/net v0.43.0 // indirect - golang.org/x/time v0.11.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/time v0.14.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect ) require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect - al.essio.dev/pkg/shellescape v1.5.1 // indirect - github.com/4meepo/tagalign v1.4.2 // indirect - github.com/Abirdcfly/dupword v0.1.3 // indirect - github.com/Antonboom/errname v1.1.0 // indirect - github.com/Antonboom/nilnil v1.1.0 // indirect - github.com/Antonboom/testifylint v1.6.1 // indirect - github.com/BurntSushi/toml v1.5.0 // indirect - github.com/Crocmagnon/fatcontext v0.7.1 // indirect - github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + codeberg.org/chavacava/garif v0.2.0 // indirect + codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect + dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect + dev.gaijin.team/go/golib v0.6.0 // indirect + github.com/4meepo/tagalign v1.4.3 // indirect + github.com/Abirdcfly/dupword v0.1.7 // indirect + github.com/AdminBenni/iota-mixing v1.0.0 // indirect + github.com/AlwxSin/noinlineerr v1.0.5 // indirect + github.com/Antonboom/errname v1.1.1 // indirect + github.com/Antonboom/nilnil v1.1.1 // indirect + github.com/Antonboom/testifylint v1.6.4 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/Djarvur/go-err113 v0.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/MirrexOne/unqueryvet v1.5.4 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect + github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect - github.com/alexkohler/nakedret/v2 v2.0.5 // indirect - github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alexkohler/nakedret/v2 v2.0.6 // indirect + github.com/alexkohler/prealloc v1.1.0 // indirect + github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect - github.com/ashanbrown/forbidigo v1.6.0 // indirect - github.com/ashanbrown/makezero v1.2.0 // indirect + github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect + github.com/ashanbrown/makezero/v2 v2.1.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect + github.com/bombsimon/wsl/v5 v5.6.0 // indirect github.com/breml/bidichk v0.3.3 // indirect github.com/breml/errchkjson v0.4.1 // indirect github.com/butuzov/ireturn v0.4.0 // indirect github.com/butuzov/mirror v1.3.0 // indirect - github.com/catenacyber/perfsprint v0.9.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/catenacyber/perfsprint v0.10.1 // indirect + github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charithe/durationcheck v0.0.10 // indirect - github.com/chavacava/garif v0.1.0 // indirect + github.com/charithe/durationcheck v0.0.11 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.6 // indirect + github.com/daixiang0/gci v0.13.7 // indirect + github.com/dave/dst v0.27.3 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.15 // indirect - github.com/go-critic/go-critic v0.13.0 // indirect + github.com/ghostiam/protogetter v0.3.20 // indirect + github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -102,162 +120,170 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/flock v0.12.1 // indirect - github.com/golang/protobuf v1.5.4 // indirect + github.com/godoc-lint/godoc-lint v0.11.2 // indirect + github.com/gofrs/flock v0.13.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect - github.com/golangci/go-printf-func-name v0.1.0 // indirect + github.com/golangci/go-printf-func-name v0.1.1 // indirect github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint v1.64.8 // indirect - github.com/golangci/misspell v0.6.0 // indirect - github.com/golangci/plugin-module-register v0.1.1 // indirect + github.com/golangci/golangci-lint/v2 v2.11.4 // indirect + github.com/golangci/golines v0.15.0 // indirect + github.com/golangci/misspell v0.8.0 // indirect + github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect - github.com/gordonklaus/ineffassign v0.1.0 // indirect + github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect + github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect + github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect - github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/gostaticanalysis/nilerr v0.1.2 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect - github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/jgautheron/goconst v1.7.1 // indirect + github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jjti/go-spancheck v0.6.4 // indirect + github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect - github.com/kisielk/errcheck v1.9.0 // indirect + github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect + github.com/kisielk/errcheck v1.10.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/kulti/thelper v0.6.3 // indirect - github.com/kunwardeep/paralleltest v1.0.14 // indirect + github.com/kulti/thelper v0.7.1 // indirect + github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.3 // indirect - github.com/ldez/gomoddirectives v0.6.1 // indirect - github.com/ldez/grignotin v0.9.0 // indirect - github.com/ldez/tagliatelle v0.7.1 // indirect - github.com/ldez/usetesting v0.4.3 // indirect + github.com/ldez/exptostd v0.4.5 // indirect + github.com/ldez/gomoddirectives v0.8.0 // indirect + github.com/ldez/grignotin v0.10.1 // indirect + github.com/ldez/structtags v0.6.1 // indirect + github.com/ldez/tagliatelle v0.7.2 // indirect + github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect - github.com/maratori/testableexamples v1.0.0 // indirect - github.com/maratori/testpackage v1.1.1 // indirect + github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect + github.com/manuelarte/funcorder v0.5.0 // indirect + github.com/maratori/testableexamples v1.0.1 // indirect + github.com/maratori/testpackage v1.1.2 // indirect github.com/matoous/godox v1.1.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.9.0 // indirect + github.com/mgechev/revive v1.15.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.19.1 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/polyfloyd/go-errorlint v1.8.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect - github.com/quasilyte/go-ruleguard v0.4.4 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect + github.com/quasilyte/go-ruleguard v0.4.5 // indirect + github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/raeperd/recvcheck v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect - github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect + github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect - github.com/securego/gosec/v2 v2.22.3 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect + github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sivchari/tenv v1.12.1 // indirect - github.com/sonatard/noctx v0.1.0 // indirect + github.com/sonatard/noctx v0.5.1 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect + github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.10.0 // indirect - github.com/tdakkota/asciicheck v0.4.1 // indirect - github.com/tetafro/godot v1.5.1 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/tetafro/godot v1.5.4 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect - github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect + github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.0 // indirect - github.com/uudashr/iface v1.3.1 // indirect + github.com/uudashr/gocognit v1.2.1 // indirect + github.com/uudashr/iface v1.4.1 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.13.1 // indirect - go-simpler.org/sloglint v0.11.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/zap v1.24.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/tools v0.36.0 // indirect - golang.org/x/tools/go/expect v0.1.1-deprecated // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect - google.golang.org/protobuf v1.36.6 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - honnef.co/go/tools v0.6.1 // indirect - mvdan.cc/gofumpt v0.8.0 // indirect - mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect + go-simpler.org/musttag v0.14.0 // indirect + go-simpler.org/sloglint v0.11.1 // indirect + go.augendre.info/arangolint v0.4.0 // indirect + go.augendre.info/fatcontext v0.9.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/tools v0.47.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + honnef.co/go/tools v0.7.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + mvdan.cc/gofumpt v0.9.2 // indirect + mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/danieljoos/wincred v1.2.2 // indirect + github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gofuzz v1.2.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1 - github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 - github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 - github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.3.1 - github.com/stackitcloud/stackit-sdk-go/services/observability v0.10.0 - github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1 - github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/stackitcloud/stackit-sdk-go/services/kms v1.11.0 + github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.14.0 + github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1 + github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.30.0 + github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.9.0 + github.com/stackitcloud/stackit-sdk-go/services/observability v0.24.0 + github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v1.1.0 + github.com/stackitcloud/stackit-sdk-go/services/redis v1.1.0 + github.com/stackitcloud/stackit-sdk-go/services/sfs v0.9.0 github.com/subosito/gotenv v1.6.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/sys v0.46.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.32.3 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + k8s.io/api v0.36.2 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) tool ( - github.com/golangci/golangci-lint/cmd/golangci-lint + github.com/golangci/golangci-lint/v2/cmd/golangci-lint golang.org/x/tools/cmd/goimports ) diff --git a/go.sum b/go.sum index 25bb26a89..2624092d1 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,6 @@ 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= -al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= -al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -36,56 +34,70 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= +codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= +codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= +codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= +dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= +dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= -github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= -github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= -github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= -github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE= -github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw= -github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng= -github.com/Antonboom/nilnil v1.1.0/go.mod h1:b7sAlogQjFa1wV8jUW3o4PMzDVFLbTux+xnQdvzdcIE= -github.com/Antonboom/testifylint v1.6.1 h1:6ZSytkFWatT8mwZlmRCHkWz1gPi+q6UBSbieji2Gj/o= -github.com/Antonboom/testifylint v1.6.1/go.mod h1:k+nEkathI2NFjKO6HvwmSrbzUcQ6FAnbZV+ZRrnXPLI= +github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= +github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= +github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= +github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= +github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= +github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= +github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= +github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= +github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= +github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= +github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= +github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= +github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= +github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM= -github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= -github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= +github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ= +github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= -github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= -github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= -github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= +github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= +github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= +github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= +github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= +github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= -github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= -github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= -github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= +github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= +github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= +github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -96,6 +108,8 @@ github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= +github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8= +github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU= github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= @@ -104,19 +118,27 @@ github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= -github.com/catenacyber/perfsprint v0.9.1 h1:5LlTp4RwTooQjJCvGEFV6XksZvWE7wCOUvjD2z0vls0= -github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= -github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= -github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= +github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= +github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= +github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= -github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= -github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= -github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= +github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -128,10 +150,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8= -github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= -github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= -github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= +github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= +github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= +github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= +github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= +github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -140,32 +166,32 @@ github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42 github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY= -github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= -github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY= -github.com/go-critic/go-critic v0.13.0/go.mod h1:M/YeuJ3vOCQDnP2SU+ZhjgRzwzcBW87JqLpMJLrZDLI= +github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= +github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= +github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= +github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -175,8 +201,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -207,23 +233,23 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= +github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM= +github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -251,28 +277,34 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= +github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= -github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= -github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= +github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I= -github.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4= -github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= -github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= -github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= -github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/golangci-lint/v2 v2.11.4 h1:GK+UlZBN5y7rh2PBnHA93XLSX6RaF7uhzJQ3JwU1wuA= +github.com/golangci/golangci-lint/v2 v2.11.4/go.mod h1:ODQDCASMA3VqfZYIbbQLpTRTzV7O/vjmIRF6u8NyFwI= +github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= +github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= +github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= +github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= +github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= -github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= +github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -283,14 +315,10 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -300,27 +328,24 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= -github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= +github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= -github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= -github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= +github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= @@ -329,8 +354,8 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -342,14 +367,14 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= -github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= -github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= -github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jedib0t/go-pretty/v6 v6.8.2 h1:FmKNr1GOyot/zqNQplE8HLhFguJaeHJTCArntnI4uxE= +github.com/jedib0t/go-pretty/v6 v6.8.2/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= +github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= -github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= +github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= +github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -364,11 +389,10 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= -github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= -github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= -github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= +github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= +github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw= +github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= @@ -382,34 +406,42 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= -github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= -github.com/kunwardeep/paralleltest v1.0.14 h1:wAkMoMeGX/kGfhQBPODT/BL8XhK23ol/nuQ3SwFaUw8= -github.com/kunwardeep/paralleltest v1.0.14/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= +github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= +github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= +github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= +github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= -github.com/ldez/exptostd v0.4.3 h1:Ag1aGiq2epGePuRJhez2mzOpZ8sI9Gimcb4Sb3+pk9Y= -github.com/ldez/exptostd v0.4.3/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= -github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= -github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= -github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= -github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= -github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= -github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= -github.com/ldez/usetesting v0.4.3 h1:pJpN0x3fMupdTf/IapYjnkhiY1nSTN+pox1/GyBRw3k= -github.com/ldez/usetesting v0.4.3/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= +github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= +github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= +github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= +github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= +github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= +github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= +github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= +github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= +github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= +github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= +github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= -github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I= +github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= -github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= -github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= -github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= +github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= +github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= +github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= +github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= +github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= +github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= +github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= @@ -418,13 +450,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.9.0 h1:8LaA62XIKrb8lM6VsBSQ92slt/o92z5+hTw3CmrvSrM= -github.com/mgechev/revive v1.9.0/go.mod h1:LAPq3+MgOf7GcL5PlWIkHb0PT7XH4NuC2LdWymhb9Mo= +github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= +github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -432,10 +463,13 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= @@ -446,14 +480,12 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4= -github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= -github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= -github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= -github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= +github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -465,15 +497,10 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/polyfloyd/go-errorlint v1.8.0 h1:DL4RestQqRLr8U4LygLw8g2DX6RN1eBJOpa2mzsrl1Q= -github.com/polyfloyd/go-errorlint v1.8.0/go.mod h1:G2W0Q5roxbLCt0ZQbdoxQxXktTjwNyDbEaj3n7jvl4s= -github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -496,10 +523,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/quasilyte/go-ruleguard v0.4.4 h1:53DncefIeLX3qEpjzlS1lyUmQoUEeOWPFWqaTJq9eAQ= -github.com/quasilyte/go-ruleguard v0.4.4/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= -github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= -github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= +github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= +github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= +github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= @@ -518,149 +545,156 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= -github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= -github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= -github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= -github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= +github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= -github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/securego/gosec/v2 v2.22.3 h1:mRrCNmRF2NgZp4RJ8oJ6yPJ7G4x6OCiAXHd8x4trLRc= -github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k= +github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= +github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= +github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 h1:AoLtJX4WUtZkhhUUMFy3GgecAALp/Mb4S1iyQOA2s0U= +github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XLCJiRE95ga77XInNELh2M6zQP+PdqiT9Zpm0D9Wpk= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= -github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= -github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= -github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs= +github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= -github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= -github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= -github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stackitcloud/stackit-sdk-go/core v0.17.3 h1:GsZGmRRc/3GJLmCUnsZswirr5wfLRrwavbnL/renOqg= -github.com/stackitcloud/stackit-sdk-go/core v0.17.3/go.mod h1:HBCXJGPgdRulplDzhrmwC+Dak9B/x0nzNtmOpu+1Ahg= -github.com/stackitcloud/stackit-sdk-go/services/alb v0.6.1 h1:wJC/7mkIppHTBU0awGdLEFcmnjasp43MM9gX6/gdWvA= -github.com/stackitcloud/stackit-sdk-go/services/alb v0.6.1/go.mod h1:UyN4hlkdiK5beDi5j9xHMENxRE9A9dlIWSXO/unhQW8= -github.com/stackitcloud/stackit-sdk-go/services/authorization v0.8.1 h1:Kzr1G4g9PHI8ePFnHrHZEX06XtEJQYBK9JExje0aXl0= -github.com/stackitcloud/stackit-sdk-go/services/authorization v0.8.1/go.mod h1:OwQ+fYpON4WQpEinvI9lCTuuwj9UBCnPPJcnDpK803U= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1 h1:CnhAMLql0MNmAeq4roQKN8OpSKX4FSgTU6Eu6detB4I= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.1/go.mod h1:7Bx85knfNSBxulPdJUFuBePXNee3cO+sOTYnUG6M+iQ= -github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1 h1:hkFixFnBcQzU4BSIZFITc8N0gK0pUYk7mk0wdUu5Ki8= -github.com/stackitcloud/stackit-sdk-go/services/git v0.7.1/go.mod h1:Ng1EzrRndG3iGXGH90AZJz//wfK+2YOyDwTnTLwX3a4= -github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.1 h1:GfE+FaeIKSVaKvgzh8Eacum+bQVyRS6ngltkh0qNGtM= -github.com/stackitcloud/stackit-sdk-go/services/iaas v0.29.1/go.mod h1:b/jgJf7QHdRzU2fmZeJJtu5j0TAevDRghzcn5MyRmOI= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1 h1:OdJEs8eOfrzn9tCBDLxIyP8hX50zPfcXNYnRoQX+chs= -github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.5.1/go.mod h1:11uzaOPCF9SeDHXEGOPMlHDD3J5r2TnvCGUwW9Igq9c= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1 h1:hv5WrRU9rN6Jx4OwdOGJRyaQrfA9p1tzEoQK6/CDyoA= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.1/go.mod h1:ivt8lvnAoBZsde2jSAuicyn6RgTmHvvNAJ3whaUbAD4= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1 h1:Db/ebOL2vbpIeh5XB2Ews2B9Lj5DJlMWIEJh60FfZ4Y= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.1/go.mod h1:8jdN4v2euK3f9gfdzbRi8e4nBJ8g/Q5YF9aPB4M4fCQ= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2 h1:BQ+qAkVS/aGHepE/+gVsvSg1sRkPOyIUI/jkCyUOrWg= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.5.2/go.mod h1:oc8Mpwl7O6EZwG0YxfhOzNCJwNQBWK5rFh764OtxoMY= -github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.3.1 h1:4jsFLbDVEosYTgQz6lPds1E9KDOiHwjuhWqcG+lo5B4= -github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.3.1/go.mod h1:j1SHAS5lN8F9b/iPUOfjAl9QAA9tOT7NKOiDEzcM2zc= -github.com/stackitcloud/stackit-sdk-go/services/observability v0.10.0 h1:SIctDqGprEoZArWaTds7hzQyh8Pqaz95Nmuj/1QuDEQ= -github.com/stackitcloud/stackit-sdk-go/services/observability v0.10.0/go.mod h1:tJEOi6L0le4yQZPGwalup/PZ13gqs1aCQDqlUs2cYW0= -github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1 h1:50n87uZn0EvSP9hJGLqd3Wm2hfqbyh7BMGGCk7axgqA= -github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.24.1/go.mod h1:jfguuSPa56Z5Bzs/Xg/CI37XzPo5Zn5lzC5LhfuT8Qc= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1 h1:K8vXele3U6b5urcSIpq21EkVblWfPDY3eMPSuQ48TkI= -github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.2.1/go.mod h1:hyhw+I19NtjKmRLcUkY4boaTxnYSPFGbpn4RxvGqH2s= -github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1 h1:ALrDCBih8Fu8e6530KdOjuH0iMxOLntO381BbKFlTFY= -github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.25.1/go.mod h1:+qGWSehoV0Js3FalgvT/bOgPj+UqW4I7lP5s8uAxP+o= -github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1 h1:8uPt82Ez34OYMOijjEYxB1zUW6kiybkt6veQKl0AL68= -github.com/stackitcloud/stackit-sdk-go/services/redis v0.25.1/go.mod h1:1Y2GEICmZDt+kr8aGnBx/sjYVAIYHmtfC8xYi9oxNEE= -github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1 h1:r7oaINTwLmIG31AaqKTuQHHFF8YNuYGzi+46DOuSjw4= -github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.17.1/go.mod h1:ipcrPRbwfQXHH18dJVfY7K5ujHF5dTT6isoXgmA7YwQ= -github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1 h1:CPIuqhQw+oPt08I2WLsxJDoVDsPMW2VkvKW7/SlUv10= -github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.3.1/go.mod h1:tip0Ob6x06luy3CmRrmnCMtU5cha95fQLdvZlno3J4w= -github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1 h1:WKFzlHllql3JsVcAq+Y1m5pSMkvwp1qH3Vf2N7i8CPg= -github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.13.1/go.mod h1:WGMFtGugBmUxI+nibI7eUZIQk4AGlDvwqX+m17W1y5w= -github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2 h1:tfKC4Z6Uah9AQZrtCn/ytqOgc//ChQRfJ6ozxovgads= -github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.3.2/go.mod h1:wV7/BUV3BCLq5+E1bHXrKKt/eOPVdWgLArWLAq7rZ/U= -github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1 h1:hcHX2n5pUsOcv2PPPbSJph1fQ/I6P7g7781T1f1ycEI= -github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.2.1/go.mod h1:jZwTg3wU4/UxgNJ7TKlFZ3dTIlnfvppnW8kJTc4UXy8= -github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.0 h1:u0PjbKDuIVOMm9hyxLeqSM51ExtJXJ+TdSJT5hDW6wk= -github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.11.0/go.mod h1:QCrAW/Rmf+styT25ke8cUV6hDHpdKNmAY14kkJ3+Fd8= -github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2 h1:s2iag/Gc4tuQH7x5I0n4mQWVhpfl/cj+SVNAFAB5ck0= -github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.2.2/go.mod h1:DFEamKVoOjm/rjMwzfZK0Zg/hwsSkXOibdA4HcC6swk= -github.com/stackitcloud/stackit-sdk-go/services/ske v1.3.0 h1:hPCpRcWEzwzGONZJsKH+j2TjN1LRTH7Tp/q0TyzmL5M= -github.com/stackitcloud/stackit-sdk-go/services/ske v1.3.0/go.mod h1:jDYRbagjOwKEVsvkxdUErXIvvTOLw9WdBVjaXr5YOD8= -github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.1 h1:r5808lRhtY8l5anft/UwgJEaef1XsoYObmwd3FVai48= -github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.3.1/go.mod h1:+LYy2pB+tpF0lkkmCf524wvv2Sa49REgEaNh7JGzN6Y= -github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= -github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10cz4l0KM2L6hqYBH2QA= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= +github.com/stackitcloud/stackit-sdk-go/services/alb v0.14.2 h1:hGzfOJjlCRoFpri5eYIiwhE27qu02pKZLprKvbsTC/w= +github.com/stackitcloud/stackit-sdk-go/services/alb v0.14.2/go.mod h1:eK6oRB5Tmpt6KbXQ4UYBGg2LgW5bPtVoncL9E8JSRww= +github.com/stackitcloud/stackit-sdk-go/services/authorization v0.12.0 h1:HxPgBu04j5tj6nfZ2r0l6v4VXC0/tYOGe4sA5Addra8= +github.com/stackitcloud/stackit-sdk-go/services/authorization v0.12.0/go.mod h1:uYI9pHAA2g84jJN25ejFUxa0/JtfpPZqMDkctQ1BzJk= +github.com/stackitcloud/stackit-sdk-go/services/cdn v1.16.0 h1:Wqxx0PDTL2F5gqI5jjznuJY0TdqECltjA0aa/rHY63U= +github.com/stackitcloud/stackit-sdk-go/services/cdn v1.16.0/go.mod h1:MHB1N3EQ9GuAduAQoNS+gb1MjrWJieszbpOso9TQv5s= +github.com/stackitcloud/stackit-sdk-go/services/dns v0.21.0 h1:ZVkptfVCAqpaPWkE+WIopM9XdzqgbVcwmX5L1jZqqx8= +github.com/stackitcloud/stackit-sdk-go/services/dns v0.21.0/go.mod h1:FiYSv3D9rzgEVzi8Mpq5oYZBosrasa5uUYqVdEIbM1U= +github.com/stackitcloud/stackit-sdk-go/services/edge v0.4.3 h1:TxChb2qbO82JiQEBYClSSD5HZxqKeKJ6dIvkEUCJmbs= +github.com/stackitcloud/stackit-sdk-go/services/edge v0.4.3/go.mod h1:KVWvQHb7CQLD9DzA4Np3WmakiCCsrHaCXvFEnOQ7nPk= +github.com/stackitcloud/stackit-sdk-go/services/git v0.14.0 h1:VZBneGprCmHqckcSMPs3puBlK8rBpLMtYKmBktwdoVE= +github.com/stackitcloud/stackit-sdk-go/services/git v0.14.0/go.mod h1:YZEL+gaK+ELn5E9VtK8yvz5RcmCBH+JkRpf6YbNVSbM= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.13.0 h1:PQZ6n71CMadLU3DjJxJXiPHdK9Bz4hMttBREQD6no34= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.13.0/go.mod h1:AbPN9BGkdjc+tVsXEX9Vr8BPDjdlDmG26K1FwCKZQVU= +github.com/stackitcloud/stackit-sdk-go/services/intake v0.7.1 h1:7ZSrwps/zI41rl+xYkG4osld8cyAwssyl/UZ/Iu/F2g= +github.com/stackitcloud/stackit-sdk-go/services/intake v0.7.1/go.mod h1:ZIvwBZwEMFO+YfJLCNXqabslI0Fp9zxV7ZBwlZjk7uE= +github.com/stackitcloud/stackit-sdk-go/services/kms v1.11.0 h1:OrUaDypQNr1nOXZfVQXCwUpN4YhR5y0vtvYi9/Ogoi4= +github.com/stackitcloud/stackit-sdk-go/services/kms v1.11.0/go.mod h1:pVaCmb1ZHAPGVRlSlBlVOjThp9Tb2sX9+nRX0M+d1KU= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.14.0 h1:1dvL7tX91ziklayQmOupniE3jM4D5Nbtc0auNcx2p18= +github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.14.0/go.mod h1:+Ld3dn648I+YKcBV3fEkYpDSr3fel421+LurJGywSBs= +github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1 h1:iteL61eMMPWT6872nF4Ko/tBU1IXemvS++XR09pj6NA= +github.com/stackitcloud/stackit-sdk-go/services/logme v1.0.1/go.mod h1:JDOOYaGgcBts2x52nKPRMFgSZe7qqOFmfz1xIXCQgRY= +github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0 h1:g7zpfQFFq3UhAWrMK9rPaZY6dLMAuMJf5g6+r7VRTXc= +github.com/stackitcloud/stackit-sdk-go/services/logs v0.10.0/go.mod h1:tvRejL8w5KpGBbLFPQ+dXOJURgZ3OMbZmwxlKQrGMuA= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.30.0 h1:LFIH1wAp633ZAJw/7H3F4nq1DZe0Qu3rlDeXhobZEZA= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.30.0/go.mod h1:joa89Y1dyn0j22FstRcIKfW2ada3FDxNfttxSvq27uY= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.0 h1:SVd3WMmLAE0Jxk2SaRuM85DTOOXHycyMpZGx9vzqNkI= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v1.12.0/go.mod h1:0hHEPiOEMAA23EzEl42Rm3FlyKIzkW+LWLvDkuFTZ+Q= +github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.9.0 h1:T+ll3lS0Kn18d8hTOrVAMKcQrXiab+Cuj0cTnAYHmj0= +github.com/stackitcloud/stackit-sdk-go/services/objectstorage v1.9.0/go.mod h1:QsPtoqAYvumyPU6ToX/5j1PbudN+VSTuvh6mp154ecM= +github.com/stackitcloud/stackit-sdk-go/services/observability v0.24.0 h1:KhuWPXr1hl8BFLGLSi6wjxh5o6OQXH9amfquMhYQROU= +github.com/stackitcloud/stackit-sdk-go/services/observability v0.24.0/go.mod h1:0fEZQHm729mBdvg4sNrAhM6KmHROHJSeS2FwCMRk46k= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.1.0 h1:hooc/E9Qabn8xno1NUd3uJQfUbW5KoY6mgURlnd776c= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v1.1.0/go.mod h1:L+NlfC1hilLOqlLLukCj/UDnxlnNrc/oMikcw3Ansyw= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.11.0 h1:cuI4NhuFhaZ3tTkBpUM7nt2odKFJkyCcphT/3gGb9CE= +github.com/stackitcloud/stackit-sdk-go/services/postgresflex v1.11.0/go.mod h1:yzlakB+f8ur4yAHR6lyCABO+HcEtZG3G2Faj6m5/uW8= +github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v1.1.0 h1:HRJwodJX4aOn/487zaqJIKw13yIj4T6dn7/kEHLxeTg= +github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v1.1.0/go.mod h1:TwfVVynB/+AKbccSOLk2qZpPL1tdK43BBAiACP6EtSg= +github.com/stackitcloud/stackit-sdk-go/services/redis v1.1.0 h1:ckYMRXAGE2/vaxPeNGEuun9AGcFn/8xuu0ytfsiqfWU= +github.com/stackitcloud/stackit-sdk-go/services/redis v1.1.0/go.mod h1:yjej6QfYoYdRIyKXlmbVz8fZYxbuUdl+QBkvLDPgA4k= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0 h1:JPP6a0ME1tZXr4iB69d/LtJsCAr58ENBadFaK9f48/c= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0/go.mod h1:NEz3f+GV5G++BE9/MmZCsXJyCih7jtg0pZuSyG2sLEs= +github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.8.0 h1:T5gy5i+NxrpJPYDEJNjGjljhfD7PWTFia7us8A4mL/c= +github.com/stackitcloud/stackit-sdk-go/services/runcommand v1.8.0/go.mod h1:iB27HtF0UcAugURc9w+nlNrtbAj7Mukw/ptAz+7p2WE= +github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.18.1 h1:U5rstX5e6Am2t+Ukv5K1Sbftzxt5aFALMa9YS4jCJoo= +github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.18.1/go.mod h1:2XA8PE05Qg6BL2YXO4XgfGI9qskJ3cicLE5Qq0aqDdY= +github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.7.0 h1:U3bm+RVHD1USpB6Fk9WNfPQxK0x0DY7ubusmRSh/A/8= +github.com/stackitcloud/stackit-sdk-go/services/serverbackup v1.7.0/go.mod h1:BJeafbecuocdirGCAJ2Vz/rPSK/LRrB5lPBEacqd1eU= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.5.2 h1:6C/iTPoYPCrZOc3JMmvyDNs4hm+JR5p9O3DIfFGgnbo= +github.com/stackitcloud/stackit-sdk-go/services/serverupdate v1.5.2/go.mod h1:/OHYZXQb9KXDdZK5J9C2YS6DJUD2i6ednZ1rK7zpDZ0= +github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.12.0 h1:l1EDIlXce2C8JcbBDHVa6nZ4SjPTqmnALTgrhms+NKI= +github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.12.0/go.mod h1:EXq8/J7t9p8zPmdIq+atuxyAbnQwxrQT18fI+Qpv98k= +github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0 h1:TNZHrunhsXRbuqZcucLs2Gqy1sEyvabufM7pB5Tscmo= +github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0/go.mod h1:fXq3TmVLb4JMSve989NFFViMFoYa83s7M3hJWgN6mdQ= +github.com/stackitcloud/stackit-sdk-go/services/sfs v0.9.0 h1:JWAFnskRbNKT8x62pZcAMCC+p5hyTEkAyxqFwy39jFA= +github.com/stackitcloud/stackit-sdk-go/services/sfs v0.9.0/go.mod h1:jMlBoXqrPNX5nXbo6oT7exalqilw1jiLPoIp4Cn0CdI= +github.com/stackitcloud/stackit-sdk-go/services/ske v1.11.0 h1:QoKyQPe8FqDqJLNgE5uRlZ/y1c1GUxjV1DDLu5QEBD8= +github.com/stackitcloud/stackit-sdk-go/services/ske v1.11.0/go.mod h1:KhVYCR58wETqdI7Quwhe3OR3BhB2T/b7DzaMsfDnr8g= +github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.11.0 h1:PwjQeupEnXxhu+uWCUzO/hUfL4yqNblOcZbP2jvaQtU= +github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex v1.11.0/go.mod h1:AiUoMAqQcOlMgDtkVJlqI7P/VGD5xjN3dYjERGnwN/M= +github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0 h1:LMgbzhPunuelsIsfyEj/5O/aYfNcg/eGHsnZ7AZOhYg= +github.com/stackitcloud/stackit-sdk-go/services/vpn v0.14.0/go.mod h1:toIjQk1dhxdUFVyCWJJja0w/0nFpDid8MWX0ukQfvfo= +github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= +github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= -github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= -github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= +github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= +github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= -github.com/tomarrell/wrapcheck/v2 v2.11.0 h1:BJSt36snX9+4WTIXeJ7nvHBQBcm1h2SjQMSlmQ6aFSU= -github.com/tomarrell/wrapcheck/v2 v2.11.0/go.mod h1:wFL9pDWDAbXhhPZZt+nG8Fu+h29TtnZ2MW6Lx4BRXIU= +github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= +github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= -github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= -github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= -github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= +github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= +github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= +github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= +github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= @@ -674,31 +708,35 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= -github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs= +github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= -go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU= -go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k= -go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco= -go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= +go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= +go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= +go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= +go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= +go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= +go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= +go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= +go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -718,12 +756,12 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -748,13 +786,11 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -789,22 +825,20 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -820,8 +854,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -865,26 +899,24 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -892,18 +924,16 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -936,34 +966,26 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= @@ -1047,8 +1069,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1056,8 +1078,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1065,9 +1087,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1077,30 +1097,32 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= -honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= -k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= -k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k= -mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg= -mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8= -mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4/go.mod h1:rthT7OuvRbaGcd5ginj6dA2oLE7YNlta9qhBNNdCaLE= +honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= +honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= +k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= +k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg= +k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ= +k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4= +k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI= +k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= +mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= -sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/golang-ci.yaml b/golang-ci.yaml index 3487f7456..3026ce631 100644 --- a/golang-ci.yaml +++ b/golang-ci.yaml @@ -1,95 +1,89 @@ -# This file contains all available configuration options -# with their default values. - -# options for analysis running +version: "2" run: - # default concurrency is a available CPU number concurrency: 4 - - # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 5m -linters-settings: - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: github.com/freiheit-com/nmww - depguard: - rules: - main: - list-mode: lax # Everything is allowed unless it is denied - deny: - - pkg: "github.com/stretchr/testify" - desc: Do not use a testing framework - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - golint: - min-confidence: 0.8 - gosec: - excludes: - # Suppressions: (see https://github.com/securego/gosec#available-rules for details) - - G104 # "Audit errors not checked" -> which we don't need and is a badly implemented version of errcheck - - G102 # "Bind to all interfaces" -> since this is normal in k8s - - G304 # "File path provided as taint input" -> too many false positives - - G307 # "Deferring unsafe method "Close" on type "io.ReadCloser" -> false positive when calling defer resp.Body.Close() - nakedret: - max-func-lines: 0 - revive: - ignore-generated-header: true - severity: error - # https://github.com/mgechev/revive - rules: - - name: errorf - - name: context-as-argument - - name: error-return - - name: increment-decrement - - name: indent-error-flow - - name: superfluous-else - - name: unused-parameter - - name: unreachable-code - - name: atomic - - name: empty-lines - - name: early-return - gocritic: - enabled-tags: - - performance - - style - - experimental - disabled-checks: - - wrapperFunc - - typeDefFirst - - ifElseChain - - dupImport # https://github.com/go-critic/go-critic/issues/845 linters: enable: - # https://golangci-lint.run/usage/linters/ - # default linters - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - - unused - # additional linters + - bodyclose + - depguard - errorlint + - forcetypeassert - gochecknoinits - gocritic - - gofmt - - goimports - gosec - misspell - nakedret - revive - - depguard - - bodyclose - sqlclosecheck - wastedassign - - forcetypeassert - - errcheck disable: - noctx # false positive: finds errors with http.NewRequest that dont make sense - unparam # false positives -issues: - exclude-use-default: false + settings: + depguard: + rules: + main: + list-mode: lax + deny: + - pkg: github.com/stretchr/testify + desc: Do not use a testing framework + gocritic: + disabled-checks: + - wrapperFunc + - typeDefFirst + - ifElseChain + - dupImport # https://github.com/go-critic/go-critic/issues/845 + - rangeValCopy + enabled-tags: + - performance + - style + - experimental + gosec: + excludes: + # Suppressions: (see https://github.com/securego/gosec#available-rules for details) + - G104 # "Audit errors not checked" -> which we don't need and is a badly implemented version of errcheck + - G102 # "Bind to all interfaces" -> since this is normal in k8s + - G304 # "File path provided as taint input" -> too many false positives + - G307 # "Deferring unsafe method "Close" on type "io.ReadCloser" -> false positive when calling defer resp.Body.Close() + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + nakedret: + max-func-lines: 0 + revive: + severity: error + # https://github.com/mgechev/revive + rules: + - name: errorf + - name: context-as-argument + - name: error-return + - name: increment-decrement + - name: indent-error-flow + - name: superfluous-else + - name: unused-parameter + - name: unreachable-code + - name: atomic + - name: empty-lines + - name: early-return + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/stackitcloud/stackit-cli + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/internal/cmd/affinity-groups/affinity-groups.go b/internal/cmd/affinity-groups/affinity-groups.go index a750b2047..0287d8713 100644 --- a/internal/cmd/affinity-groups/affinity-groups.go +++ b/internal/cmd/affinity-groups/affinity-groups.go @@ -2,16 +2,17 @@ package affinity_groups import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/affinity-groups/create" "github.com/stackitcloud/stackit-cli/internal/cmd/affinity-groups/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/affinity-groups/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/affinity-groups/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "affinity-group", Short: "Manage server affinity groups", @@ -23,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( create.NewCmd(params), delete.NewCmd(params), diff --git a/internal/cmd/affinity-groups/create/create.go b/internal/cmd/affinity-groups/create/create.go index 60b14fdd1..6528e54f8 100644 --- a/internal/cmd/affinity-groups/create/create.go +++ b/internal/cmd/affinity-groups/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Policy string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates an affinity groups", @@ -42,9 +42,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit affinity-group create --name AFFINITY_GROUP_NAME --policy soft-affinity", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -55,12 +55,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create the affinity group %q?", model.Name) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create the affinity group %q?", model.Name) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -90,17 +88,17 @@ func configureFlags(cmd *cobra.Command) { } func buildRequest(ctx context.Context, model inputModel, apiClient *iaas.APIClient) iaas.ApiCreateAffinityGroupRequest { - req := apiClient.CreateAffinityGroup(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreateAffinityGroup(ctx, model.ProjectId, model.Region) req = req.CreateAffinityGroupPayload( iaas.CreateAffinityGroupPayload{ - Name: utils.Ptr(model.Name), - Policy: utils.Ptr(model.Policy), + Name: model.Name, + Policy: model.Policy, }, ) return req } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -112,38 +110,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Policy: flags.FlagToStringValue(p, cmd, policyFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func outputResult(p *print.Printer, model inputModel, resp iaas.AffinityGroup) error { outputFormat := "" if model.GlobalFlagModel != nil { - outputFormat = model.GlobalFlagModel.OutputFormat + outputFormat = model.OutputFormat } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal affinity group: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal affinity group: %w", err) - } - p.Outputln(string(details)) - default: + + return p.OutputResult(outputFormat, resp, func() error { p.Outputf("Created affinity group %q with id %s\n", model.Name, utils.PtrString(resp.Id)) - } - return nil + return nil + }) } diff --git a/internal/cmd/affinity-groups/create/create_test.go b/internal/cmd/affinity-groups/create/create_test.go index 0f71d31db..e547a4990 100644 --- a/internal/cmd/affinity-groups/create/create_test.go +++ b/internal/cmd/affinity-groups/create/create_test.go @@ -7,31 +7,33 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -const projectIdFlag = globalflags.ProjectIdFlag +const ( + testName = "test-name" + testPolicy = "test-policy" + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() ) -const ( - testName = "test-name" - testPolicy = "test-policy" -) - func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, nameFlag: testName, policyFlag: testPolicy, @@ -47,6 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Name: testName, Policy: testPolicy, @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateAffinityGroupRequest)) iaas.ApiCreateAffinityGroupRequest { - request := testClient.CreateAffinityGroup(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateAffinityGroup(testCtx, testProjectId, testRegion) request = request.CreateAffinityGroupPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -68,8 +71,8 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateAffinityGroupRequest)) i func fixturePayload(mods ...func(payload *iaas.CreateAffinityGroupPayload)) iaas.CreateAffinityGroupPayload { payload := iaas.CreateAffinityGroupPayload{ - Name: utils.Ptr(testName), - Policy: utils.Ptr(testPolicy), + Name: testName, + Policy: testPolicy, } for _, mod := range mods { mod(&payload) @@ -80,6 +83,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateAffinityGroupPayload)) iaas func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,43 +124,7 @@ func TestParseInput(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - if err := cmd.Flags().Set(flag, value); err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +147,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx)) + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{})) if diff != "" { t.Fatalf("Request does not match: %s", diff) } @@ -205,18 +173,17 @@ func TestOutputResult(t *testing.T) { model: *fixtureInputModel(), response: iaas.AffinityGroup{ Id: utils.Ptr(testProjectId), - Members: utils.Ptr([]string{uuid.NewString(), uuid.NewString()}), - Name: utils.Ptr("test-project"), - Policy: utils.Ptr("hard-affinity"), + Members: []string{uuid.NewString(), uuid.NewString()}, + Name: "test-project", + Policy: "hard-affinity", }, isValid: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := outputResult(p, tt.model, tt.response) + err := outputResult(params.Printer, tt.model, tt.response) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/affinity-groups/delete/delete.go b/internal/cmd/affinity-groups/delete/delete.go index a05c2e7bc..87f6b2487 100644 --- a/internal/cmd/affinity-groups/delete/delete.go +++ b/internal/cmd/affinity-groups/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { @@ -27,7 +29,7 @@ const ( affinityGroupIdArg = "AFFINITY_GROUP" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", affinityGroupIdArg), Short: "Deletes an affinity group", @@ -58,18 +60,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - affinityGroupLabel, err := iaasUtils.GetAffinityGroupName(ctx, apiClient, model.ProjectId, model.AffinityGroupId) + affinityGroupLabel, err := iaasUtils.GetAffinityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.AffinityGroupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get affinity group name: %v", err) affinityGroupLabel = model.AffinityGroupId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete affinity group %q?", affinityGroupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete affinity group %q?", affinityGroupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -87,7 +87,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func buildRequest(ctx context.Context, model inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteAffinityGroupRequest { - return apiClient.DeleteAffinityGroup(ctx, model.ProjectId, model.AffinityGroupId) + return apiClient.DefaultAPI.DeleteAffinityGroup(ctx, model.ProjectId, model.Region, model.AffinityGroupId) } func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputModel, error) { @@ -101,14 +101,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM AffinityGroupId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/affinity-groups/delete/delete_test.go b/internal/cmd/affinity-groups/delete/delete_test.go index e3df49cbb..67ed31ab9 100644 --- a/internal/cmd/affinity-groups/delete/delete_test.go +++ b/internal/cmd/affinity-groups/delete/delete_test.go @@ -7,19 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -const projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testAffinityGroupId = uuid.NewString() @@ -37,7 +40,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -50,6 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, AffinityGroupId: testAffinityGroupId, } @@ -60,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteAffinityGroupRequest)) iaas.ApiDeleteAffinityGroupRequest { - request := testClient.DeleteAffinityGroup(testCtx, testProjectId, testAffinityGroupId) + request := testClient.DefaultAPI.DeleteAffinityGroup(testCtx, testProjectId, testRegion, testAffinityGroupId) for _, mod := range mods { mod(&request) } @@ -97,8 +102,8 @@ func TestParseInput(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -130,7 +135,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -167,7 +172,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/affinity-groups/describe/describe.go b/internal/cmd/affinity-groups/describe/describe.go index 6abd3980c..a2f377f36 100644 --- a/internal/cmd/affinity-groups/describe/describe.go +++ b/internal/cmd/affinity-groups/describe/describe.go @@ -2,12 +2,14 @@ package describe import ( "context" - "encoding/json" "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { @@ -28,7 +29,7 @@ const ( affinityGroupId = "AFFINITY_GROUP_ID" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", affinityGroupId), Short: "Show details of an affinity group", @@ -70,7 +71,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func buildRequest(ctx context.Context, model inputModel, apiClient *iaas.APIClient) iaas.ApiGetAffinityGroupRequest { - return apiClient.GetAffinityGroup(ctx, model.ProjectId, model.AffinityGroupId) + return apiClient.DefaultAPI.GetAffinityGroup(ctx, model.ProjectId, model.Region, model.AffinityGroupId) } func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputModel, error) { @@ -84,59 +85,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM AffinityGroupId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func outputResult(p *print.Printer, model inputModel, resp iaas.AffinityGroup) error { var outputFormat string if model.GlobalFlagModel != nil { - outputFormat = model.GlobalFlagModel.OutputFormat + outputFormat = model.OutputFormat } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal affinity group: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal affinity group: %w", err) - } - p.Outputln(string(details)) - default: + + return p.OutputResult(outputFormat, resp, func() error { table := tables.NewTable() if resp.HasId() { table.AddRow("ID", utils.PtrString(resp.Id)) table.AddSeparator() } - if resp.Name != nil { - table.AddRow("NAME", utils.PtrString(resp.Name)) - table.AddSeparator() - } - if resp.Policy != nil { - table.AddRow("POLICY", utils.PtrString(resp.Policy)) - table.AddSeparator() - } + table.AddRow("NAME", resp.Name) + table.AddSeparator() + table.AddRow("POLICY", resp.Policy) + table.AddSeparator() if resp.HasMembers() { - table.AddRow("Members", utils.JoinStringPtr(resp.Members, ", ")) + table.AddRow("Members", strings.Join(resp.Members, ", ")) table.AddSeparator() } if err := table.Display(p); err != nil { return fmt.Errorf("render table: %w", err) } - } - return nil + return nil + }) } diff --git a/internal/cmd/affinity-groups/describe/describe_test.go b/internal/cmd/affinity-groups/describe/describe_test.go index 530319c96..116ed2f8f 100644 --- a/internal/cmd/affinity-groups/describe/describe_test.go +++ b/internal/cmd/affinity-groups/describe/describe_test.go @@ -7,19 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -const projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( - testCtx = context.WithValue(context.Background(), &testCtxKey{}, projectIdFlag) - testClient = &iaas.APIClient{} + testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testAffinityGroupId = uuid.NewString() @@ -37,7 +40,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -50,6 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, AffinityGroupId: testAffinityGroupId, } @@ -60,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetAffinityGroupRequest)) iaas.ApiGetAffinityGroupRequest { - request := testClient.GetAffinityGroup(testCtx, testProjectId, testAffinityGroupId) + request := testClient.DefaultAPI.GetAffinityGroup(testCtx, testProjectId, testRegion, testAffinityGroupId) for _, mod := range mods { mod(&request) } @@ -98,8 +103,8 @@ func TestParseInput(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -131,7 +136,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -168,7 +173,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,12 +196,11 @@ func TestOutputResult(t *testing.T) { response: iaas.AffinityGroup{}, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := outputResult(p, tt.model, tt.response) + err := outputResult(params.Printer, tt.model, tt.response) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/affinity-groups/list/list.go b/internal/cmd/affinity-groups/list/list.go index 5665a970c..006a94fb5 100644 --- a/internal/cmd/affinity-groups/list/list.go +++ b/internal/cmd/affinity-groups/list/list.go @@ -2,23 +2,24 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { @@ -28,7 +29,7 @@ type inputModel struct { const limitFlag = "limit" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists affinity groups", @@ -44,9 +45,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit affinity-group list --limit=10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -63,16 +64,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list affinity groups: %w", err) } + items := result.GetItems() - if items := result.Items; items != nil { - if model.Limit != nil && len(*items) > int(*model.Limit) { - *items = (*items)[:*model.Limit] - } - return outputResult(params.Printer, *model, *items) + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - params.Printer.Outputln("No affinity groups found") - return nil + // Truncate Output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -84,10 +88,10 @@ func configureFlags(cmd *cobra.Command) { } func buildRequest(ctx context.Context, model inputModel, apiClient *iaas.APIClient) iaas.ApiListAffinityGroupsRequest { - return apiClient.ListAffinityGroups(ctx, model.ProjectId) + return apiClient.DefaultAPI.ListAffinityGroups(ctx, model.ProjectId, model.Region) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -106,44 +110,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func outputResult(p *print.Printer, model inputModel, items []iaas.AffinityGroup) error { - var outputFormat string - if model.GlobalFlagModel != nil { - outputFormat = model.GlobalFlagModel.OutputFormat - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal affinity groups: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal affinity groups: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []iaas.AffinityGroup) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No affinity groups found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "POLICY") for _, item := range items { table.AddRow( utils.PtrString(item.Id), - utils.PtrString(item.Name), - utils.PtrString(item.Policy), + item.Name, + item.Policy, ) table.AddSeparator() } @@ -151,6 +134,7 @@ func outputResult(p *print.Printer, model inputModel, items []iaas.AffinityGroup if err := table.Display(p); err != nil { return fmt.Errorf("render table: %w", err) } - } - return nil + + return nil + }) } diff --git a/internal/cmd/affinity-groups/list/list_test.go b/internal/cmd/affinity-groups/list/list_test.go index b44af626c..973445dcd 100644 --- a/internal/cmd/affinity-groups/list/list_test.go +++ b/internal/cmd/affinity-groups/list/list_test.go @@ -8,30 +8,32 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -const projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" + testLimit = 10 +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() ) -const ( - testLimit = 10 -) - func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -44,6 +46,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, } for _, mod := range mods { @@ -53,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListAffinityGroupsRequest)) iaas.ApiListAffinityGroupsRequest { - request := testClient.ListAffinityGroups(testCtx, testProjectId) + request := testClient.DefaultAPI.ListAffinityGroups(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +66,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListAffinityGroupsRequest)) ia func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -105,43 +109,7 @@ func TestParseInput(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - if err := cmd.Flags().Set(flag, value); err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -164,7 +132,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx)) + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{})) if diff != "" { t.Fatalf("Request does not match: %s", diff) } @@ -173,24 +141,40 @@ func TestBuildRequest(t *testing.T) { } func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + instances []iaas.AffinityGroup + } tests := []struct { description string - model inputModel - response []iaas.AffinityGroup + args args isValid bool }{ { description: "empty", - model: inputModel{}, - response: []iaas.AffinityGroup{}, + args: args{}, isValid: true, }, + { + description: "empty slice", + args: args{ + instances: []iaas.AffinityGroup{}, + }, + isValid: true, + }, + { + description: "empty affinity group in slice", + args: args{ + instances: []iaas.AffinityGroup{{}}, + }, + isValid: true, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := outputResult(p, tt.model, tt.response) + err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/auth/activate-service-account/activate_service_account.go b/internal/cmd/auth/activate-service-account/activate_service_account.go index 5ddc73f29..aa17b6def 100644 --- a/internal/cmd/auth/activate-service-account/activate_service_account.go +++ b/internal/cmd/auth/activate-service-account/activate_service_account.go @@ -4,8 +4,10 @@ import ( "errors" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" @@ -23,6 +25,7 @@ const ( serviceAccountTokenFlag = "service-account-token" serviceAccountKeyPathFlag = "service-account-key-path" privateKeyPathFlag = "private-key-path" + useOIDCFlag = "use-oidc" onlyPrintAccessTokenFlag = "only-print-access-token" // #nosec G101 ) @@ -30,10 +33,11 @@ type inputModel struct { ServiceAccountToken string ServiceAccountKeyPath string PrivateKeyPath string + UseOIDC *bool OnlyPrintAccessToken bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "activate-service-account", Short: "Authenticates using a service account", @@ -57,9 +61,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Only print the corresponding access token by using the service account token. This access token can be stored as environment variable (STACKIT_ACCESS_TOKEN) in order to be used for all subsequent commands.`, "$ stackit auth activate-service-account --service-account-token my-service-account-token --only-print-access-token", ), + examples.NewExample( + `Authenticate via Workload Identity Federation (OIDC) and print the short-lived access token. Use --use-oidc to explicitly enable OIDC (takes precedence over STACKIT_USE_OIDC); no service account key file is required.`, + "$ STACKIT_SERVICE_ACCOUNT_EMAIL=ci@sa.stackit.cloud stackit auth activate-service-account --use-oidc --only-print-access-token", + ), ), - RunE: func(cmd *cobra.Command, _ []string) error { - model := parseInput(params.Printer, cmd) + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // use workload identity federation (OIDC) if enabled; no key file required + if auth.IsOIDCEnabledWithOverride(model.UseOIDC) { + return runOIDCMode(params, model) + } tokenCustomEndpoint := viper.GetString(config.TokenCustomEndpointKey) if !model.OnlyPrintAccessToken { @@ -110,29 +126,73 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(serviceAccountTokenFlag, "", "Service account long-lived access token") cmd.Flags().String(serviceAccountKeyPathFlag, "", "Service account key path") cmd.Flags().String(privateKeyPathFlag, "", "RSA private key path. It takes precedence over the private key included in the service account key, if present") + cmd.Flags().Bool(useOIDCFlag, false, "Use Workload Identity Federation (OIDC). If set, this takes precedence over STACKIT_USE_OIDC") cmd.Flags().Bool(onlyPrintAccessTokenFlag, false, "If this is set to true the credentials are not stored in either the keyring or a file") } -func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { model := inputModel{ ServiceAccountToken: flags.FlagToStringValue(p, cmd, serviceAccountTokenFlag), ServiceAccountKeyPath: flags.FlagToStringValue(p, cmd, serviceAccountKeyPathFlag), PrivateKeyPath: flags.FlagToStringValue(p, cmd, privateKeyPathFlag), OnlyPrintAccessToken: flags.FlagToBoolValue(p, cmd, onlyPrintAccessTokenFlag), } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + if cmd.Flags().Changed(useOIDCFlag) { + useOIDC := flags.FlagToBoolValue(p, cmd, useOIDCFlag) + model.UseOIDC = &useOIDC } - return &model + p.DebugInputModel(model) + return &model, nil } func storeCustomEndpoint(tokenCustomEndpoint string) error { return auth.SetAuthField(auth.TOKEN_CUSTOM_ENDPOINT, tokenCustomEndpoint) } + +func runOIDCMode(params *types.CmdParams, model *inputModel) error { + email := auth.OIDCServiceAccountEmail() + if email == "" { + return fmt.Errorf( + "env var %s must be set when %s is enabled", + auth.EnvServiceAccountEmail, auth.EnvUseOIDC, + ) + } + + tokenFunc, err := auth.OIDCTokenFunc() + if err != nil { + return err + } + + tokenCustomEndpoint := viper.GetString(config.TokenCustomEndpointKey) + + wifCfg := &sdkConfig.Configuration{ + WorkloadIdentityFederation: true, + ServiceAccountEmail: email, + ServiceAccountFederatedTokenFunc: tokenFunc, + TokenCustomUrl: tokenCustomEndpoint, + } + + rt, err := sdkAuth.SetupAuth(wifCfg) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "setup workload identity federation auth: %v", err) + return &cliErr.ActivateServiceAccountError{} + } + + // credentials are never written to disk in OIDC mode + saEmail, accessToken, err := auth.AuthenticateServiceAccount(params.Printer, rt, true) + if err != nil { + var activateErr *cliErr.ActivateServiceAccountError + if !errors.As(err, &activateErr) { + return fmt.Errorf("authenticate service account via workload identity federation: %w", err) + } + return err + } + + if model.OnlyPrintAccessToken { + params.Printer.Outputf("%s\n", accessToken) + } else { + params.Printer.Outputf("Authenticated via Workload Identity Federation.\nService account email: %s\n", saEmail) + } + return nil +} diff --git a/internal/cmd/auth/activate-service-account/activate_service_account_test.go b/internal/cmd/auth/activate-service-account/activate_service_account_test.go index 84532195c..be10fc8e2 100644 --- a/internal/cmd/auth/activate-service-account/activate_service_account_test.go +++ b/internal/cmd/auth/activate-service-account/activate_service_account_test.go @@ -3,15 +3,14 @@ package activateserviceaccount import ( "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/zalando/go-keyring" - "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) var testTokenCustomEndpoint = "token_url" @@ -21,6 +20,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st serviceAccountTokenFlag: "token", serviceAccountKeyPathFlag: "sa_key", privateKeyPathFlag: "private_key", + useOIDCFlag: "true", onlyPrintAccessTokenFlag: "true", } for _, mod := range mods { @@ -34,6 +34,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ServiceAccountToken: "token", ServiceAccountKeyPath: "sa_key", PrivateKeyPath: "private_key", + UseOIDC: utils.Ptr(true), OnlyPrintAccessToken: true, } for _, mod := range mods { @@ -45,6 +46,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string tokenCustomEndpoint string isValid bool @@ -66,6 +68,7 @@ func TestParseInput(t *testing.T) { ServiceAccountToken: "", ServiceAccountKeyPath: "", PrivateKeyPath: "", + UseOIDC: nil, }, }, { @@ -81,8 +84,29 @@ func TestParseInput(t *testing.T) { ServiceAccountToken: "", ServiceAccountKeyPath: "", PrivateKeyPath: "", + UseOIDC: nil, }, }, + { + description: "use_oidc_true", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[useOIDCFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.UseOIDC = utils.Ptr(true) + }), + }, + { + description: "use_oidc_false", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[useOIDCFlag] = "false" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.UseOIDC = utils.Ptr(false) + }), + }, { description: "invalid_flag", flagValues: fixtureFlagValues(func(flagValues map[string]string) { @@ -102,36 +126,23 @@ func TestParseInput(t *testing.T) { model.OnlyPrintAccessToken = false }), }, + { + description: "default value UseOIDC", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + delete(flagValues, useOIDCFlag) + }, + ), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.UseOIDC = nil + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - model := parseInput(p, cmd) - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/auth/auth.go b/internal/cmd/auth/auth.go index 7e1c020cf..d54f3fb01 100644 --- a/internal/cmd/auth/auth.go +++ b/internal/cmd/auth/auth.go @@ -5,14 +5,14 @@ import ( getaccesstoken "github.com/stackitcloud/stackit-cli/internal/cmd/auth/get-access-token" "github.com/stackitcloud/stackit-cli/internal/cmd/auth/login" "github.com/stackitcloud/stackit-cli/internal/cmd/auth/logout" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "auth", Short: "Authenticates the STACKIT CLI", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(login.NewCmd(params)) cmd.AddCommand(logout.NewCmd(params)) cmd.AddCommand(activateserviceaccount.NewCmd(params)) diff --git a/internal/cmd/auth/get-access-token/get_access_token.go b/internal/cmd/auth/get-access-token/get_access_token.go index f5040d85c..bf97df4ec 100644 --- a/internal/cmd/auth/get-access-token/get_access_token.go +++ b/internal/cmd/auth/get-access-token/get_access_token.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -18,7 +20,7 @@ type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "get-access-token", Short: "Prints a short-lived access token.", @@ -29,8 +31,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Print a short-lived access token`, "$ stackit auth get-access-token"), ), - RunE: func(cmd *cobra.Command, _ []string) error { - model, err := parseInput(params.Printer, cmd) + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -45,7 +47,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { accessToken, err := auth.GetValidAccessToken(params.Printer) if err != nil { - return err + params.Printer.Debug(print.ErrorLevel, "get valid access token: %v", err) + return &cliErr.SessionExpiredError{} } switch model.OutputFormat { @@ -66,24 +69,23 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } }, } + + // hide project id flag from help command because it could mislead users + cmd.SetHelpFunc(func(command *cobra.Command, strings []string) { + _ = command.Flags().MarkHidden(globalflags.ProjectIdFlag) // nolint:errcheck // there's no chance to handle the error here + command.Parent().HelpFunc()(command, strings) + }) + return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := inputModel{ GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/auth/login/login.go b/internal/cmd/auth/login/login.go index 8740fead7..8a03d19af 100644 --- a/internal/cmd/auth/login/login.go +++ b/internal/cmd/auth/login/login.go @@ -3,15 +3,25 @@ package login import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +const ( + portFlag = "port" +) + +type inputModel struct { + Port *int +} + +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "login", Short: "Logs in to the STACKIT CLI", @@ -24,8 +34,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Login to the STACKIT CLI. This command will open a browser window where you can login to your STACKIT account`, "$ stackit auth login"), ), - RunE: func(_ *cobra.Command, _ []string) error { - err := auth.AuthorizeUser(params.Printer, false) + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + err = auth.AuthorizeUser(params.Printer, auth.UserAuthConfig{ + IsReauthentication: false, + Port: model.Port, + }) if err != nil { return fmt.Errorf("authorization failed: %w", err) } @@ -35,5 +53,28 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return nil }, } + configureFlags(cmd) return cmd } + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int(portFlag, 0, + "The port on which the callback server will listen to. By default, it tries to bind a port between 8000 and 8020.\n"+ + "When a value is specified, it will only try to use the specified port. Valid values are within the range of 8000 to 8020.", + ) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + port := flags.FlagToIntPointer(p, cmd, portFlag) + // For the CLI client only callback URLs with localhost:[8000-8020] are valid. Additional callbacks must be enabled in the backend. + if port != nil && (*port < 8000 || 8020 < *port) { + return nil, fmt.Errorf("port must be between 8000 and 8020") + } + + model := inputModel{ + Port: port, + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/auth/login/login_test.go b/internal/cmd/auth/login/login_test.go new file mode 100644 index 000000000..823fa863e --- /dev/null +++ b/internal/cmd/auth/login/login_test.go @@ -0,0 +1,93 @@ +package login + +import ( + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + portFlag: "8010", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + Port: utils.Ptr(8010), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: &inputModel{ + Port: nil, + }, + }, + { + description: "lower limit", + flagValues: map[string]string{ + portFlag: "8000", + }, + isValid: true, + expectedModel: &inputModel{ + Port: utils.Ptr(8000), + }, + }, + { + description: "below lower limit is not valid ", + flagValues: map[string]string{ + portFlag: "7999", + }, + isValid: false, + }, + { + description: "upper limit", + flagValues: map[string]string{ + portFlag: "8020", + }, + isValid: true, + expectedModel: &inputModel{ + Port: utils.Ptr(8020), + }, + }, + { + description: "above upper limit is not valid ", + flagValues: map[string]string{ + portFlag: "8021", + }, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} diff --git a/internal/cmd/auth/logout/logout.go b/internal/cmd/auth/logout/logout.go index 4b318b2e7..e5e4f6be8 100644 --- a/internal/cmd/auth/logout/logout.go +++ b/internal/cmd/auth/logout/logout.go @@ -3,7 +3,8 @@ package logout import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -11,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "logout", Short: "Logs the user account out of the STACKIT CLI", diff --git a/internal/cmd/beta/alb/alb.go b/internal/cmd/beta/alb/alb.go index fde1da311..62bd90d2b 100644 --- a/internal/cmd/beta/alb/alb.go +++ b/internal/cmd/beta/alb/alb.go @@ -11,15 +11,15 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/quotas" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/template" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "alb", Short: "Manages application loadbalancers", @@ -31,7 +31,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( list.NewCmd(params), template.NewCmd(params), diff --git a/internal/cmd/beta/alb/create/create.go b/internal/cmd/beta/alb/create/create.go index 2622a3ce4..4b8e1700b 100644 --- a/internal/cmd/beta/alb/create/create.go +++ b/internal/cmd/beta/alb/create/create.go @@ -8,8 +8,10 @@ import ( "os" "strings" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +24,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" - "github.com/stackitcloud/stackit-sdk-go/services/alb/wait" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api/wait" ) const ( @@ -35,7 +37,7 @@ type inputModel struct { Configuration *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates an application loadbalancer", @@ -46,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create an application loadbalancer from a configuration file`, "$ stackit beta alb create --configuration my-loadbalancer.json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -65,12 +67,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create an application loadbalancer for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create an application loadbalancer for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -85,13 +85,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating loadbalancer") - _, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *resp.Name).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating loadbalancer", func() error { + _, err := wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *resp.Name).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for loadbalancer creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, resp) @@ -107,7 +107,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,20 +118,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiCreateLoadBalancerRequest, err error) { - req = apiClient.CreateLoadBalancer(ctx, model.ProjectId, model.Region) + req = apiClient.DefaultAPI.CreateLoadBalancer(ctx, model.ProjectId, model.Region) payload, err := readPayload(ctx, model) if err != nil { return req, err @@ -170,29 +162,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp if resp == nil { return fmt.Errorf("create loadbalancer response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal loadbalancer: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal loadbalancer: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } p.Outputf("%s application loadbalancer for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name)) return nil - } + }) } diff --git a/internal/cmd/beta/alb/create/create_test.go b/internal/cmd/beta/alb/create/create_test.go index 82517b0f5..0eefec8b1 100644 --- a/internal/cmd/beta/alb/create/create_test.go +++ b/internal/cmd/beta/alb/create/create_test.go @@ -10,11 +10,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) //go:embed testdata/testconfig.json @@ -25,7 +27,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &alb.APIClient{} +var testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testRegion = "eu01" var testConfig = "testdata/testconfig.json" @@ -67,7 +69,7 @@ func fixturePayload(mods ...func(payload *alb.CreateLoadBalancerPayload)) (paylo } func fixtureRequest(mods ...func(request *alb.ApiCreateLoadBalancerRequest)) alb.ApiCreateLoadBalancerRequest { - request := testClient.CreateLoadBalancer(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateLoadBalancer(testCtx, testProjectId, testRegion) request = request.CreateLoadBalancerPayload(fixturePayload()) for _, mod := range mods { @@ -79,6 +81,7 @@ func fixtureRequest(mods ...func(request *alb.ApiCreateLoadBalancerRequest)) alb func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -134,46 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +163,7 @@ func TestBuildRequest(t *testing.T) { }, Configuration: &testConfig, }, - expectedRequest: testClient. + expectedRequest: testClient.DefaultAPI. CreateLoadBalancer(testCtx, testProjectId, testRegion). CreateLoadBalancerPayload(fixturePayload()), }, @@ -213,7 +177,7 @@ func TestBuildRequest(t *testing.T) { } diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -248,11 +212,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/delete/delete.go b/internal/cmd/beta/alb/delete/delete.go index 25886cd40..3f88b8274 100644 --- a/internal/cmd/beta/alb/delete/delete.go +++ b/internal/cmd/beta/alb/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -13,7 +14,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -25,7 +26,7 @@ type inputModel struct { Name string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", loadbalancerNameArg), Short: "Deletes an application loadbalancer", @@ -56,12 +57,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the application loadbalancer %q for project %q?", model.Name, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -71,7 +70,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("delete loadbalancer: %w", err) } - params.Printer.Outputf("Load balancer %q deleted.", model.Name) + params.Printer.Outputf("Load balancer %q deleted.\n", model.Name) return nil }, } @@ -87,18 +86,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Name: loadbalancerName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiDeleteLoadBalancerRequest { - return apiClient.DeleteLoadBalancer(ctx, model.ProjectId, model.Region, model.Name) + return apiClient.DefaultAPI.DeleteLoadBalancer(ctx, model.ProjectId, model.Region, model.Name) } diff --git a/internal/cmd/beta/alb/delete/delete_test.go b/internal/cmd/beta/alb/delete/delete_test.go index ce6808fbd..d06dad9ce 100644 --- a/internal/cmd/beta/alb/delete/delete_test.go +++ b/internal/cmd/beta/alb/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} @@ -20,7 +19,7 @@ var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") testProjectId = uuid.NewString() testRegion = "eu01" - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testLoadBalancerName = "my-test-loadbalancer" ) @@ -61,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiDeleteLoadBalancerRequest)) alb.ApiDeleteLoadBalancerRequest { - request := testClient.DeleteLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) + request := testClient.DefaultAPI.DeleteLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) for _, mod := range mods { mod(&request) } @@ -112,54 +111,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) }) } } @@ -182,7 +134,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedResult, - cmp.AllowUnexported(tt.expectedResult), + cmp.AllowUnexported(tt.expectedResult, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/beta/alb/describe/describe.go b/internal/cmd/beta/alb/describe/describe.go index 67a10bf4a..0afd8a722 100644 --- a/internal/cmd/beta/alb/describe/describe.go +++ b/internal/cmd/beta/alb/describe/describe.go @@ -2,11 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -16,9 +16,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Name string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", loadbalancerNameArg), Short: "Describes an application loadbalancer", @@ -81,70 +80,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Name: loadbalancerName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetLoadBalancerRequest { - return apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.Name) + return apiClient.DefaultAPI.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.Name) } -func outputResult(p *print.Printer, outputFormat string, response *alb.LoadBalancer) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(response, "", " ") +func outputResult(p *print.Printer, outputFormat string, loadbalancer *alb.LoadBalancer) error { + return p.OutputResult(outputFormat, loadbalancer, func() error { + content := []tables.Table{} - if err != nil { - return fmt.Errorf("marshal loadbalancer: %w", err) + content = append(content, buildLoadBalancerTable(loadbalancer)) + + if loadbalancer.Listeners != nil { + content = append(content, buildListenersTable(loadbalancer.Listeners)) } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if loadbalancer.TargetPools != nil { + content = append(content, buildTargetPoolsTable(loadbalancer.TargetPools)) + } + err := tables.DisplayTables(p, content) if err != nil { - return fmt.Errorf("marshal loadbalancer: %w", err) + return fmt.Errorf("display output: %w", err) } - p.Outputln(string(details)) return nil - default: - if err := outputResultAsTable(p, response); err != nil { - return err - } - } - - return nil -} - -func outputResultAsTable(p *print.Printer, loadbalancer *alb.LoadBalancer) error { - content := []tables.Table{} - - content = append(content, buildLoadBalancerTable(loadbalancer)) - - if loadbalancer.Listeners != nil { - content = append(content, buildListenersTable(*loadbalancer.Listeners)) - } - - if loadbalancer.TargetPools != nil { - content = append(content, buildTargetPoolsTable(*loadbalancer.TargetPools)) - } - - err := tables.DisplayTables(p, content) - if err != nil { - return fmt.Errorf("display output: %w", err) - } - - return nil + }) } func buildLoadBalancerTable(loadbalancer *alb.LoadBalancer) tables.Table { @@ -152,7 +116,7 @@ func buildLoadBalancerTable(loadbalancer *alb.LoadBalancer) tables.Table { privateAccessOnly := false if loadbalancer.Options != nil { if loadbalancer.Options.AccessControl != nil && loadbalancer.Options.AccessControl.AllowedSourceRanges != nil { - acl = *loadbalancer.Options.AccessControl.AllowedSourceRanges + acl = loadbalancer.Options.AccessControl.AllowedSourceRanges } if loadbalancer.Options.PrivateNetworkOnly != nil { @@ -161,16 +125,16 @@ func buildLoadBalancerTable(loadbalancer *alb.LoadBalancer) tables.Table { } networkId := "-" - if loadbalancer.Networks != nil && len(*loadbalancer.Networks) > 0 { - networks := *loadbalancer.Networks + if len(loadbalancer.Networks) > 0 { + networks := loadbalancer.Networks networkId = *networks[0].NetworkId } externalAddress := utils.PtrStringDefault(loadbalancer.ExternalAddress, "-") errorDescriptions := []string{} - if loadbalancer.Errors != nil && len((*loadbalancer.Errors)) > 0 { - for _, err := range *loadbalancer.Errors { + if len(loadbalancer.Errors) > 0 { + for _, err := range loadbalancer.Errors { errorDescriptions = append(errorDescriptions, *err.Description) } } @@ -215,7 +179,7 @@ func buildTargetPoolsTable(targetPools []alb.TargetPool) tables.Table { table.SetTitle("Target Pools") table.SetHeader("NAME", "PORT", "TARGETS") for _, targetPool := range targetPools { - table.AddRow(utils.PtrString(targetPool.Name), utils.PtrString(targetPool.TargetPort), len(*targetPool.Targets)) + table.AddRow(utils.PtrString(targetPool.Name), utils.PtrString(targetPool.TargetPort), len(targetPool.Targets)) } return table } diff --git a/internal/cmd/beta/alb/describe/describe_test.go b/internal/cmd/beta/alb/describe/describe_test.go index 4d1195313..e8bb8a363 100644 --- a/internal/cmd/beta/alb/describe/describe_test.go +++ b/internal/cmd/beta/alb/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} @@ -20,7 +20,7 @@ var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") testProjectId = uuid.NewString() testRegion = "eu01" - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testLoadBalancerName = "my-test-loadbalancer" ) @@ -61,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiGetLoadBalancerRequest)) alb.ApiGetLoadBalancerRequest { - request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) + request := testClient.DefaultAPI.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) for _, mod := range mods { mod(&request) } @@ -112,54 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) }) } } @@ -182,7 +135,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedResult, - cmp.AllowUnexported(tt.expectedResult), + cmp.AllowUnexported(tt.expectedResult, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -213,11 +166,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/list/list.go b/internal/cmd/beta/alb/list/list.go index 6df67a81a..6852f329e 100644 --- a/internal/cmd/beta/alb/list/list.go +++ b/internal/cmd/beta/alb/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) type inputModel struct { @@ -27,11 +27,10 @@ type inputModel struct { } const ( - labelSelectorFlag = "label-selector" - limitFlag = "limit" + limitFlag = "limit" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists albs", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit beta alb list --limit=10`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -75,19 +74,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list load balancerse: %w", err) } + items := response.GetLoadBalancers() - if items := response.LoadBalancers; items == nil || len(*items) == 0 { - params.Printer.Info("No load balancers found for project %q", projectLabel) - } else { - if model.Limit != nil && len(*items) > int(*model.Limit) { - *items = (*items)[:*model.Limit] - } - if err := outputResult(params.Printer, model.OutputFormat, *items); err != nil { - return fmt.Errorf("output loadbalancers: %w", err) - } + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] } - return nil + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } @@ -99,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,48 +112,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListLoadBalancersRequest { - request := apiClient.ListLoadBalancers(ctx, model.ProjectId, model.Region) + request := apiClient.DefaultAPI.ListLoadBalancers(ctx, model.ProjectId, model.Region) return request } -func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalancer) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal loadbalancer list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal loadbalancer list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []alb.LoadBalancer) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No load balancers found for project %q", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("NAME", "EXTERNAL ADDRESS", "REGION", "STATUS", "VERSION", "ERRORS") - for _, item := range items { + for i := range items { + item := &items[i] + var errNo int if item.Errors != nil { - errNo = len(*item.Errors) + errNo = len(item.Errors) } table.AddRow(utils.PtrString(item.Name), utils.PtrString(item.ExternalAddress), @@ -175,5 +151,5 @@ func outputResult(p *print.Printer, outputFormat string, items []alb.LoadBalance } return nil - } + }) } diff --git a/internal/cmd/beta/alb/list/list_test.go b/internal/cmd/beta/alb/list/list_test.go index f0fb05146..bf52b6666 100644 --- a/internal/cmd/beta/alb/list/list_test.go +++ b/internal/cmd/beta/alb/list/list_test.go @@ -5,24 +5,31 @@ import ( "strconv" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} - testProjectId = uuid.NewString() - testRegion = "eu01" - testLimit int64 = 10 + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testLimit int64 = 10 ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -40,7 +47,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, - Limit: &testLimit, + Limit: utils.Ptr(testLimit), } for _, mod := range mods { mod(model) @@ -49,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiListLoadBalancersRequest)) alb.ApiListLoadBalancersRequest { - request := testClient.ListLoadBalancers(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListLoadBalancers(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +66,7 @@ func fixtureRequest(mods ...func(request *alb.ApiListLoadBalancersRequest)) alb. func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -99,44 +107,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -158,7 +129,7 @@ func TestBuildRequest(t *testing.T) { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -171,6 +142,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []alb.LoadBalancer } tests := []struct { @@ -195,11 +167,11 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/observability-credentials/add/add.go b/internal/cmd/beta/alb/observability-credentials/add/add.go index 86b6ac2b9..b0d31771d 100644 --- a/internal/cmd/beta/alb/observability-credentials/add/add.go +++ b/internal/cmd/beta/alb/observability-credentials/add/add.go @@ -2,10 +2,10 @@ package add import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -13,9 +13,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { Password *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Adds observability credentials to an application load balancer", @@ -40,12 +39,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag`, - "$ stackit beta alb observability-credentials add --username xxx --password @./password.txt --display-name yyy"), + "$ stackit beta alb observability-credentials add --username xxx --password @./password.txt --displayname yyy"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -56,12 +55,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := "Are your sure you want to add credentials?" - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := "Are your sure you want to add credentials?" + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -71,45 +68,39 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("add credential: %w", err) } - return outputResult(params.Printer, model.GlobalFlagModel.OutputFormat, resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } - configureFlags(cmd) + configureFlags(cmd, params) return cmd } -func configureFlags(cmd *cobra.Command) { +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials") cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials") - cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`) + + password := flags.SecretFlag(passwordFlag, params) + cmd.Flags().Var(password, passwordFlag, password.Usage()) cobra.CheckErr(flags.MarkFlagsRequired(cmd, usernameFlag, displaynameFlag)) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := inputModel{ GlobalFlagModel: globalFlags, Username: flags.FlagToStringPointer(p, cmd, usernameFlag), Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag), - Password: flags.FlagToStringPointer(p, cmd, passwordFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string fo debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.Region) payload := alb.CreateCredentialsPayload{ DisplayName: model.Displayname, Password: model.Password, @@ -123,25 +114,10 @@ func outputResult(p *print.Printer, outputFormat string, item *alb.CreateCredent return fmt.Errorf("no credential found") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(item, "", " ") - if err != nil { - return fmt.Errorf("marshal credential: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(item, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal credential: %w", err) - } - p.Outputln(string(details)) - default: + return p.OutputResult(outputFormat, item, func() error { if item.Credential != nil { - p.Outputf("Created credential %s\n", - utils.PtrString(item.Credential.CredentialsRef), - ) + p.Outputf("Created credential %s\n", utils.PtrString(item.Credential.CredentialsRef)) } - } - return nil + return nil + }) } diff --git a/internal/cmd/beta/alb/observability-credentials/add/add_test.go b/internal/cmd/beta/alb/observability-credentials/add/add_test.go index b5fa2bac6..776cbbff1 100644 --- a/internal/cmd/beta/alb/observability-credentials/add/add_test.go +++ b/internal/cmd/beta/alb/observability-credentials/add/add_test.go @@ -4,20 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &alb.APIClient{} +var testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} var ( testProjectId = uuid.NewString() @@ -59,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiCreateCredentialsRequest)) alb.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testRegion) request = request.CreateCredentialsPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -82,6 +82,7 @@ func fixturePayload(mods ...func(payload *alb.CreateCredentialsPayload)) alb.Cre func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -101,46 +102,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -163,7 +125,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), cmp.AllowUnexported(alb.NullableString{}), ) @@ -202,11 +164,10 @@ func Test_outputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.item); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.item); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/observability-credentials/delete/delete.go b/internal/cmd/beta/alb/observability-credentials/delete/delete.go index ccf998580..29ff84d7e 100644 --- a/internal/cmd/beta/alb/observability-credentials/delete/delete.go +++ b/internal/cmd/beta/alb/observability-credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -12,7 +13,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -24,7 +25,7 @@ type inputModel struct { CredentialsRef string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialRefArg), Short: "Deletes credentials", @@ -49,12 +50,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %q?", model.CredentialsRef) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -82,18 +81,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsRef: credentialRef, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiDeleteCredentialsRequest { - return apiClient.DeleteCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) + return apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) } diff --git a/internal/cmd/beta/alb/observability-credentials/delete/delete_test.go b/internal/cmd/beta/alb/observability-credentials/delete/delete_test.go index 09ecaa532..f92158a49 100644 --- a/internal/cmd/beta/alb/observability-credentials/delete/delete_test.go +++ b/internal/cmd/beta/alb/observability-credentials/delete/delete_test.go @@ -4,14 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) type testCtxKey struct{} @@ -20,7 +21,7 @@ var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") testProjectId = uuid.NewString() testRegion = "eu01" - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testCredentialRef = "credential-12345" ) @@ -61,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiDeleteCredentialsRequest)) alb.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialRef) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialRef) for _, mod := range mods { mod(&request) } @@ -112,8 +113,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -145,7 +146,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -182,7 +183,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/beta/alb/observability-credentials/describe/describe.go b/internal/cmd/beta/alb/observability-credentials/describe/describe.go index 001e20278..b8d8c20c1 100644 --- a/internal/cmd/beta/alb/observability-credentials/describe/describe.go +++ b/internal/cmd/beta/alb/observability-credentials/describe/describe.go @@ -2,10 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -15,9 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -29,7 +28,7 @@ type inputModel struct { CredentialRef string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialRefArg), Short: "Describes observability credentials for the Application Load Balancer", @@ -80,43 +79,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialRef: credentialRef, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetCredentialsRequest { - return apiClient.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialRef) + return apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialRef) } func outputResult(p *print.Printer, outputFormat string, response alb.CredentialsResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(response, "", " ") - - if err != nil { - return fmt.Errorf("marshal credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - - if err != nil { - return fmt.Errorf("marshal credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, response, func() error { table := tables.NewTable() table.AddRow("CREDENTIAL REF", utils.PtrString(response.CredentialsRef)) table.AddSeparator() @@ -128,7 +100,6 @@ func outputResult(p *print.Printer, outputFormat string, response alb.Credential table.AddSeparator() p.Outputln(table.Render()) - } - - return nil + return nil + }) } diff --git a/internal/cmd/beta/alb/observability-credentials/describe/describe_test.go b/internal/cmd/beta/alb/observability-credentials/describe/describe_test.go index 79412281b..e8126c21f 100644 --- a/internal/cmd/beta/alb/observability-credentials/describe/describe_test.go +++ b/internal/cmd/beta/alb/observability-credentials/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} @@ -20,7 +20,7 @@ var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") testProjectId = uuid.NewString() testRegion = "eu01" - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testCredentialRef = "credential-12345" ) @@ -61,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiGetCredentialsRequest)) alb.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testRegion, testCredentialRef) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testRegion, testCredentialRef) for _, mod := range mods { mod(&request) } @@ -112,54 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) }) } } @@ -182,7 +135,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedResult, - cmp.AllowUnexported(tt.expectedResult), + cmp.AllowUnexported(tt.expectedResult, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -213,11 +166,11 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/observability-credentials/list/list.go b/internal/cmd/beta/alb/observability-credentials/list/list.go index 32baa31c6..4eb068fbd 100644 --- a/internal/cmd/beta/alb/observability-credentials/list/list.go +++ b/internal/cmd/beta/alb/observability-credentials/list/list.go @@ -2,10 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -17,9 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials", @@ -51,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit beta alb observability-credentials list --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,13 +69,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list credentials: %w", err) } + items := resp.GetCredentials() - if resp.Credentials == nil || len(*resp.Credentials) == 0 { - params.Printer.Info("No credentials found\n") - return nil - } - - items := *resp.Credentials + // Truncate output if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } @@ -92,7 +87,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Number of credentials to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) @@ -108,44 +103,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.InfoLevel, modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, items []alb.CredentialsResponse) error { - if items == nil { - p.Outputln("no credentials found") - return nil - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal credentials: %w", err) - } - p.Outputln(string(details)) - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal credentials: %w", err) + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No credentials found\n") + return nil } - p.Outputln(string(details)) - default: table := tables.NewTable() table.SetHeader("CREDENTIAL REF", "DISPLAYNAME", "USERNAME", "REGION") @@ -159,6 +132,6 @@ func outputResult(p *print.Printer, outputFormat string, items []alb.Credentials } p.Outputln(table.Render()) - } - return nil + return nil + }) } diff --git a/internal/cmd/beta/alb/observability-credentials/list/list_test.go b/internal/cmd/beta/alb/observability-credentials/list/list_test.go index 77863ded8..da08c86f1 100644 --- a/internal/cmd/beta/alb/observability-credentials/list/list_test.go +++ b/internal/cmd/beta/alb/observability-credentials/list/list_test.go @@ -5,22 +5,22 @@ import ( "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testProjectId = uuid.NewString() testRegion = "eu01" @@ -55,7 +55,7 @@ func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiListCredentialsRequest)) alb.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -65,6 +65,7 @@ func fixtureRequest(mods ...func(request *alb.ApiListCredentialsRequest)) alb.Ap func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,46 +121,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatal("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -182,7 +144,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -216,10 +178,9 @@ func Test_outputResult(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() - if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/observability-credentials/observability-credentials.go b/internal/cmd/beta/alb/observability-credentials/observability-credentials.go index 3d18486ee..2c0b7d461 100644 --- a/internal/cmd/beta/alb/observability-credentials/observability-credentials.go +++ b/internal/cmd/beta/alb/observability-credentials/observability-credentials.go @@ -3,16 +3,17 @@ package credentials import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + add "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/add" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/list" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/observability-credentials/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "observability-credentials", Short: "Provides functionality for application loadbalancer credentials", @@ -24,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(add.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/beta/alb/observability-credentials/update/update.go b/internal/cmd/beta/alb/observability-credentials/update/update.go index 18b68ccb4..f81af524e 100644 --- a/internal/cmd/beta/alb/observability-credentials/update/update.go +++ b/internal/cmd/beta/alb/observability-credentials/update/update.go @@ -2,10 +2,10 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -15,9 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -35,7 +34,7 @@ type inputModel struct { CredentialsRef *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", credentialRefArg), Short: "Update credentials", @@ -65,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return fmt.Errorf("update credential: %w", err) - } + prompt := fmt.Sprintf("Are you sure you want to update credential %q for %q?", *model.CredentialsRef, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return fmt.Errorf("update credential: %w", err) } // Call API @@ -85,20 +82,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return outputResult(params.Printer, model, resp) }, } - configureFlags(cmd) + configureFlags(cmd, params) return cmd } -func configureFlags(cmd *cobra.Command) { +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials") cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials") - cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`) + password := flags.SecretFlag(passwordFlag, params) + cmd.Flags().Var(password, passwordFlag, password.Usage()) cobra.CheckErr(flags.MarkFlagsRequired(cmd, displaynameFlag, usernameFlag)) } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) (req alb.ApiUpdateCredentialsRequest, err error) { - req = apiClient.UpdateCredentials(ctx, model.ProjectId, model.Region, *model.CredentialsRef) + req = apiClient.DefaultAPI.UpdateCredentials(ctx, model.ProjectId, model.Region, *model.CredentialsRef) payload := alb.UpdateCredentialsPayload{ DisplayName: model.Displayname, @@ -119,44 +117,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) inputM Username: flags.FlagToStringPointer(p, cmd, usernameFlag), Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag), CredentialsRef: &inputArgs[0], - Password: flags.FlagToStringPointer(p, cmd, passwordFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag), } + p.DebugInputModel(model) return model } func outputResult(p *print.Printer, model inputModel, response *alb.UpdateCredentialsResponse) error { var outputFormat string if model.GlobalFlagModel != nil { - outputFormat = model.GlobalFlagModel.OutputFormat + outputFormat = model.OutputFormat } if response == nil { - return fmt.Errorf("no response passewd") + return fmt.Errorf("no response passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(response.Credential, "", " ") - if err != nil { - return fmt.Errorf("marshal credential: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(response.Credential, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal credential: %w", err) - } - p.Outputln(string(details)) - default: + + return p.OutputResult(outputFormat, response.Credential, func() error { p.Outputf("Updated credential %q\n", utils.PtrString(model.CredentialsRef)) - } - return nil + return nil + }) } diff --git a/internal/cmd/beta/alb/observability-credentials/update/update_test.go b/internal/cmd/beta/alb/observability-credentials/update/update_test.go index 4d3780ba1..adb786bd5 100644 --- a/internal/cmd/beta/alb/observability-credentials/update/update_test.go +++ b/internal/cmd/beta/alb/observability-credentials/update/update_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &alb.APIClient{} +var testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} var ( testProjectId = uuid.NewString() @@ -61,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) inputModel { } func fixtureRequest(mods ...func(request *alb.ApiUpdateCredentialsRequest)) alb.ApiUpdateCredentialsRequest { - request := testClient.UpdateCredentials(testCtx, testProjectId, testRegion, testCredentialRef) + request := testClient.DefaultAPI.UpdateCredentials(testCtx, testProjectId, testRegion, testCredentialRef) request = request.UpdateCredentialsPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -84,6 +83,7 @@ func fixturePayload(mods ...func(payload *alb.UpdateCredentialsPayload)) alb.Upd func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string args []string isValid bool @@ -126,8 +126,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -151,7 +151,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model := parseInput(p, cmd, tt.args) + model := parseInput(params.Printer, cmd, tt.args) if !tt.isValid { t.Fatalf("did not fail on invalid input") @@ -185,7 +185,7 @@ func TestBuildRequest(t *testing.T) { } diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), cmp.AllowUnexported(alb.NullableString{}), ) @@ -216,11 +216,10 @@ func Test_outputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.item); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.item); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/plans/plans.go b/internal/cmd/beta/alb/plans/plans.go index d131422ad..4fc79a8c1 100644 --- a/internal/cmd/beta/alb/plans/plans.go +++ b/internal/cmd/beta/alb/plans/plans.go @@ -2,12 +2,13 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,14 +18,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists the application load balancer plans", @@ -36,9 +36,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit beta alb plans`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,23 +64,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list plans: %w", err) } + items := response.GetValidPlans() - if items := response.ValidPlans; items == nil || len(*items) == 0 { - params.Printer.Info("No plans found for project %q", projectLabel) - } else { - if err := outputResult(params.Printer, model.OutputFormat, *items); err != nil { - return fmt.Errorf("output plans: %w", err) - } - } - - return nil + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -90,43 +83,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiListPlansRequest { - request := apiClient.ListPlans(ctx, model.Region) + request := apiClient.DefaultAPI.ListPlans(ctx, model.Region) return request } -func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal plans: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal plans: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []alb.PlanDetails) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No plans found for project %q", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("PLAN ID", "NAME", "FLAVOR", "MAX CONNS", "DESCRIPTION") for _, item := range items { @@ -143,5 +116,5 @@ func outputResult(p *print.Printer, outputFormat string, items []alb.PlanDetails } return nil - } + }) } diff --git a/internal/cmd/beta/alb/plans/plans_test.go b/internal/cmd/beta/alb/plans/plans_test.go index 84b5c9002..96ae5818a 100644 --- a/internal/cmd/beta/alb/plans/plans_test.go +++ b/internal/cmd/beta/alb/plans/plans_test.go @@ -4,25 +4,27 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testProjectId = uuid.NewString() - testRegion = "eu01" ) +const testRegion = "eu01" + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, @@ -45,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListPlansRequest { - request := testClient.ListPlans(testCtx, testRegion) + request := testClient.DefaultAPI.ListPlans(testCtx, testRegion) for _, mod := range mods { mod(&request) } @@ -55,6 +57,7 @@ func fixtureRequest(mods ...func(request *alb.ApiListPlansRequest)) alb.ApiListP func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -95,44 +98,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -154,7 +120,7 @@ func TestBuildRequest(t *testing.T) { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -167,6 +133,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []alb.PlanDetails } tests := []struct { @@ -191,11 +158,11 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/pool/pool.go b/internal/cmd/beta/alb/pool/pool.go index d40f8733a..f83b7728b 100644 --- a/internal/cmd/beta/alb/pool/pool.go +++ b/internal/cmd/beta/alb/pool/pool.go @@ -2,15 +2,15 @@ package pool import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb/pool/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "pool", Short: "Manages target pools for application loadbalancers", @@ -22,6 +22,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(update.NewCmd(params)) } diff --git a/internal/cmd/beta/alb/pool/update/update.go b/internal/cmd/beta/alb/pool/update/update.go index 2dc8cd3a2..719c38bfd 100644 --- a/internal/cmd/beta/alb/pool/update/update.go +++ b/internal/cmd/beta/alb/pool/update/update.go @@ -8,8 +8,10 @@ import ( "os" "strings" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,7 +23,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) const ( @@ -35,7 +37,7 @@ type inputModel struct { AlbName *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Updates an application target pool", @@ -44,11 +46,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Example: examples.Build( examples.NewExample( `Update an application target pool from a configuration file (the name of the pool is read from the file)`, - "$ stackit beta alb update --configuration my-target-pool.json --name my-load-balancer"), + "$ stackit beta alb pool update --configuration my-target-pool.json --name my-load-balancer"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -65,12 +67,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update an application target pool for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update an application target pool for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -92,12 +92,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(configurationFlag, "c", "", "Filename of the input configuration file") - cmd.Flags().StringP(albNameFlag, "n", "", "Name of the target pool name to update") + cmd.Flags().StringP(albNameFlag, "n", "", "Name of the application load balancer") err := flags.MarkFlagsRequired(cmd, configurationFlag, albNameFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -109,15 +109,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { AlbName: flags.FlagToStringPointer(p, cmd, albNameFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -129,7 +121,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClie if payload.Name == nil { return req, fmt.Errorf("update target pool: no poolname provided") } - req = apiClient.UpdateTargetPool(ctx, model.ProjectId, model.Region, *model.AlbName, *payload.Name) + req = apiClient.DefaultAPI.UpdateTargetPool(ctx, model.ProjectId, model.Region, *model.AlbName, *payload.Name) return req.UpdateTargetPoolPayload(payload), nil } @@ -164,29 +156,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp if resp == nil { return fmt.Errorf("update target pool response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal target pool: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal target pool: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Updated" if model.Async { operationState = "Triggered update of" } p.Outputf("%s application target pool for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name)) return nil - } + }) } diff --git a/internal/cmd/beta/alb/pool/update/update_test.go b/internal/cmd/beta/alb/pool/update/update_test.go index f279c1f01..b749fa6ad 100644 --- a/internal/cmd/beta/alb/pool/update/update_test.go +++ b/internal/cmd/beta/alb/pool/update/update_test.go @@ -10,11 +10,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) //go:embed testdata/testconfig.json @@ -26,7 +28,7 @@ type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testProjectId = uuid.NewString() testRegion = "eu01" testLoadBalancer = "my-load-balancer" @@ -73,7 +75,7 @@ func fixturePayload(mods ...func(payload *alb.UpdateTargetPoolPayload)) (payload } func fixtureRequest(mods ...func(request *alb.ApiUpdateTargetPoolRequest)) alb.ApiUpdateTargetPoolRequest { - request := testClient.UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool) + request := testClient.DefaultAPI.UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool) request = request.UpdateTargetPoolPayload(fixturePayload()) for _, mod := range mods { @@ -85,6 +87,7 @@ func fixtureRequest(mods ...func(request *alb.ApiUpdateTargetPoolRequest)) alb.A func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -142,46 +145,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -208,7 +172,7 @@ func TestBuildRequest(t *testing.T) { Configuration: &testConfig, AlbName: &testLoadBalancer, }, - expectedRequest: testClient. + expectedRequest: testClient.DefaultAPI. UpdateTargetPool(testCtx, testProjectId, testRegion, testLoadBalancer, testPool). UpdateTargetPoolPayload(fixturePayload()), }, @@ -222,7 +186,7 @@ func TestBuildRequest(t *testing.T) { } diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -257,11 +221,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/quotas/quotas.go b/internal/cmd/beta/alb/quotas/quotas.go index c0ce6948b..9257568b4 100644 --- a/internal/cmd/beta/alb/quotas/quotas.go +++ b/internal/cmd/beta/alb/quotas/quotas.go @@ -2,12 +2,13 @@ package quotas import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,14 +17,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/alb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "quotas", Short: "Shows the application load balancer quotas", @@ -35,9 +35,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit beta alb quotas`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,7 +72,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -82,43 +82,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClient) alb.ApiGetQuotaRequest { - request := apiClient.GetQuota(ctx, model.ProjectId, model.Region) + request := apiClient.DefaultAPI.GetQuota(ctx, model.ProjectId, model.Region) return request } func outputResult(p *print.Printer, outputFormat string, response alb.GetQuotaResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(response, "", " ") - if err != nil { - return fmt.Errorf("marshal quotas: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(response, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal quotas: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, response, func() error { table := tables.NewTable() table.AddRow("REGION", utils.PtrString(response.Region)) table.AddSeparator() @@ -129,5 +104,5 @@ func outputResult(p *print.Printer, outputFormat string, response alb.GetQuotaRe } return nil - } + }) } diff --git a/internal/cmd/beta/alb/quotas/quotas_test.go b/internal/cmd/beta/alb/quotas/quotas_test.go index 6240e20d3..663d6c152 100644 --- a/internal/cmd/beta/alb/quotas/quotas_test.go +++ b/internal/cmd/beta/alb/quotas/quotas_test.go @@ -4,21 +4,22 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testProjectId = uuid.NewString() testRegion = "eu01" ) @@ -45,7 +46,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *alb.ApiGetQuotaRequest)) alb.ApiGetQuotaRequest { - request := testClient.GetQuota(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.GetQuota(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -55,6 +56,7 @@ func fixtureRequest(mods ...func(request *alb.ApiGetQuotaRequest)) alb.ApiGetQuo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -95,44 +97,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -154,7 +119,7 @@ func TestBuildRequest(t *testing.T) { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -191,11 +156,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.response); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/alb/template/template-alb.yaml b/internal/cmd/beta/alb/template/template-alb.yaml index 541aaf3de..87d5c9b7f 100644 --- a/internal/cmd/beta/alb/template/template-alb.yaml +++ b/internal/cmd/beta/alb/template/template-alb.yaml @@ -4,7 +4,7 @@ externalAddress: 123.123.123.123 # public listening interfaces of the loadbalancer listeners: - - displayName: listener1 + - name: listener1 # for plain http the https block must be removed # http: {} https: @@ -16,27 +16,28 @@ listeners: port: 443 # protocal may be PROTOCOL_HTTPS or PROTOCOL_HTTP protocol: PROTOCOL_HTTPS - rules: - # fqdn of the virtual host of the load balancer - - host: front.facing.host - http: - subRules: + http: + hosts: + # fqdn of the virtual host of the load balancer + - host: front.facing.host + rules: - cookiePersistence: name: cookie1 ttl: 120s headers: - - name: testheader1 - exactMatch: X-test-header1 - - name: testheader2 - exactMatch: X-test-header2 - - name: testheader3 - exactMatch: X-test-header3 - pathPrefix: /foo + - exactMatch: X-test-header1 + name: testheader1 + - exactMatch: X-test-header2 + name: testheader2 + - exactMatch: X-test-header3 + name: testheader3 + path: + prefix: /foo queryParameters: - - name: query-param - exactMatch: q - - name: region - exactMatch: region + - exactMatch: q + name: query-param + - exactMatch: region + name: region targetPool: my-target-pool webSocket: false networks: @@ -50,7 +51,7 @@ options: allowedSourceRanges: - 10.100.42.0/24 ephemeralAddress: true - privateNetworkOnly: true + privateNetworkOnly: false # Enable observability features. Appropriate credentials must be made # available using the credentials endpoint diff --git a/internal/cmd/beta/alb/template/template.go b/internal/cmd/beta/alb/template/template.go index c33ca2846..ad607875c 100644 --- a/internal/cmd/beta/alb/template/template.go +++ b/internal/cmd/beta/alb/template/template.go @@ -4,11 +4,13 @@ import ( _ "embed" "encoding/json" "fmt" - "os" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,12 +18,25 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) -const ( - formatFlag = "format" - typeFlag = "type" +var ( + formatFlag = flags.StringEnumFlag( + "format", + []string{"json", "yaml"}, + "Defines the output format", + flags.StringEnumIgnoreCase[string](), + flags.StringEnumShortHand[string]("f"), + flags.StringEnumDefaultValue("json"), + ) + typeFlag = flags.StringEnumFlag( + "type", + []string{"alb", "pool"}, + "Defines the output type", + flags.StringEnumIgnoreCase[string](), + flags.StringEnumShortHand[string]("t"), + flags.StringEnumDefaultValue("alb"), + ) ) type inputModel struct { @@ -37,7 +52,7 @@ var ( templatePool string ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "template", Short: "creates configuration templates to use for resource creation", @@ -53,8 +68,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit beta alb template --format=json --type pool`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { - model, err := parseInput(params.Printer, cmd) + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -79,7 +94,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err := yaml.Unmarshal([]byte(template), &target); err != nil { return fmt.Errorf("cannot unmarshal template: %w", err) } - encoder := json.NewEncoder(os.Stdout) + encoder := json.NewEncoder(params.Printer.StdOut) if err := encoder.Encode(target); err != nil { return fmt.Errorf("cannot marshal template to yaml: %w", err) } @@ -96,11 +111,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().VarP(flags.EnumFlag(true, "json", "json", "yaml"), formatFlag, "f", "Defines the output format ('yaml' or 'json'), default is 'json'") - cmd.Flags().VarP(flags.EnumFlag(true, "alb", "alb", "pool"), typeFlag, "t", "Defines the output type ('alb' or 'pool'), default is 'alb'") + formatFlag.Register(cmd.Flags()) + typeFlag.Register(cmd.Flags()) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -108,18 +123,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - Format: flags.FlagToStringPointer(p, cmd, formatFlag), - Type: flags.FlagToStringPointer(p, cmd, typeFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Format: formatFlag.Ptr(), + Type: typeFlag.Ptr(), } + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/beta/alb/template/template_test.go b/internal/cmd/beta/alb/template/template_test.go index be7bed72d..7f22c3b1c 100644 --- a/internal/cmd/beta/alb/template/template_test.go +++ b/internal/cmd/beta/alb/template/template_test.go @@ -3,12 +3,10 @@ package template import ( "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" ) @@ -31,6 +29,8 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + Format: utils.Ptr("json"), + Type: utils.Ptr("alb"), } for _, mod := range mods { mod(model) @@ -41,6 +41,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -80,8 +81,8 @@ func TestParseInput(t *testing.T) { { description: "alb with yaml", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[formatFlag] = "yaml" - flagValues[typeFlag] = "alb" + flagValues[formatFlag.Name()] = "yaml" + flagValues[typeFlag.Name()] = "alb" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -91,8 +92,8 @@ func TestParseInput(t *testing.T) { }, { description: "alb with yaml", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[formatFlag] = "yaml" - flagValues[typeFlag] = "alb" + flagValues[formatFlag.Name()] = "yaml" + flagValues[typeFlag.Name()] = "alb" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -102,8 +103,8 @@ func TestParseInput(t *testing.T) { }, { description: "alb with json", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[formatFlag] = "json" - flagValues[typeFlag] = "alb" + flagValues[formatFlag.Name()] = "json" + flagValues[typeFlag.Name()] = "alb" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -113,8 +114,8 @@ func TestParseInput(t *testing.T) { }, { description: "pool with yaml", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[formatFlag] = "yaml" - flagValues[typeFlag] = "pool" + flagValues[formatFlag.Name()] = "yaml" + flagValues[typeFlag.Name()] = "pool" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -125,8 +126,8 @@ func TestParseInput(t *testing.T) { { description: "pool with json", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[formatFlag] = "json" - flagValues[typeFlag] = "pool" + flagValues[formatFlag.Name()] = "json" + flagValues[typeFlag.Name()] = "pool" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -138,44 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/beta/alb/update/update.go b/internal/cmd/beta/alb/update/update.go index 2dcfddbb1..abbe70d08 100644 --- a/internal/cmd/beta/alb/update/update.go +++ b/internal/cmd/beta/alb/update/update.go @@ -8,8 +8,10 @@ import ( "os" "strings" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +24,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/alb" - "github.com/stackitcloud/stackit-sdk-go/services/alb/wait" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api/wait" ) const ( @@ -36,7 +38,7 @@ type inputModel struct { Version *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Updates an application loadbalancer", @@ -47,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Update an application loadbalancer from a configuration file`, "$ stackit beta alb update --configuration my-loadbalancer.json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,12 +68,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update an application loadbalancer for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update an application loadbalancer for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // for updates of an existing ALB the current version must be passed to the request @@ -91,14 +91,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("updating loadbalancer") - _, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *resp.Name). - WaitWithContext(ctx) + err := spinner.Run(params.Printer, "updating loadbalancer", func() error { + _, err = wait.CreateOrUpdateLoadbalancerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *resp.Name). + WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for loadbalancer update: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, resp) @@ -114,7 +114,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -125,15 +125,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Configuration: flags.FlagToStringPointer(p, cmd, configurationFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -149,7 +141,7 @@ func getCurrentAlbVersion(ctx context.Context, apiClient *alb.APIClient, model * if err != nil { return nil, err } - resp, err := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, *updatePayload.Name).Execute() + resp, err := apiClient.DefaultAPI.GetLoadBalancer(ctx, model.ProjectId, model.Region, *updatePayload.Name).Execute() if err != nil { return nil, err } @@ -165,7 +157,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *alb.APIClie return req, fmt.Errorf("no name found in loadbalancer configuration") } payload.Version = model.Version - req = apiClient.UpdateLoadBalancer(ctx, model.ProjectId, model.Region, *payload.Name) + req = apiClient.DefaultAPI.UpdateLoadBalancer(ctx, model.ProjectId, model.Region, *payload.Name) return req.UpdateLoadBalancerPayload(payload), nil } @@ -200,29 +192,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp if resp == nil { return fmt.Errorf("update loadbalancer response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal loadbalancer: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal loadbalancer: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Updated" if model.Async { operationState = "Triggered update of" } p.Outputf("%s application loadbalancer for %q. Name: %s\n", operationState, projectLabel, utils.PtrString(resp.Name)) return nil - } + }) } diff --git a/internal/cmd/beta/alb/update/update_test.go b/internal/cmd/beta/alb/update/update_test.go index 5749b8b86..292e78915 100644 --- a/internal/cmd/beta/alb/update/update_test.go +++ b/internal/cmd/beta/alb/update/update_test.go @@ -10,11 +10,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/alb" ) //go:embed testdata/testconfig.json @@ -26,7 +28,7 @@ type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &alb.APIClient{} + testClient = &alb.APIClient{DefaultAPI: &alb.DefaultAPIService{}} testProjectId = uuid.NewString() testRegion = "eu01" testLoadBalancer = "my-load-balancer" @@ -70,7 +72,7 @@ func fixturePayload(mods ...func(payload *alb.UpdateLoadBalancerPayload)) (paylo } func fixtureRequest(mods ...func(request *alb.ApiUpdateLoadBalancerRequest)) alb.ApiUpdateLoadBalancerRequest { - request := testClient.UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer) + request := testClient.DefaultAPI.UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer) request = request.UpdateLoadBalancerPayload(fixturePayload()) for _, mod := range mods { @@ -82,6 +84,7 @@ func fixtureRequest(mods ...func(request *alb.ApiUpdateLoadBalancerRequest)) alb func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -137,46 +140,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,7 +166,7 @@ func TestBuildRequest(t *testing.T) { }, Configuration: &testConfig, }, - expectedRequest: testClient. + expectedRequest: testClient.DefaultAPI. UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancer). UpdateLoadBalancerPayload(fixturePayload()), }, @@ -216,7 +180,7 @@ func TestBuildRequest(t *testing.T) { } diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, alb.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -251,11 +215,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/beta.go b/internal/cmd/beta/beta.go index 5a007b87e..f739b0c03 100644 --- a/internal/cmd/beta/beta.go +++ b/internal/cmd/beta/beta.go @@ -3,9 +3,15 @@ package beta import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/alb" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -13,7 +19,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "beta", Short: "Contains beta STACKIT CLI commands", @@ -35,7 +41,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(sqlserverflex.NewCmd(params)) + cmd.AddCommand(sfs.NewCmd(params)) cmd.AddCommand(alb.NewCmd(params)) + cmd.AddCommand(edge.NewCmd(params)) + cmd.AddCommand(intake.NewCmd(params)) + cmd.AddCommand(cdn.NewCmd(params)) + cmd.AddCommand(vpn.NewCmd(params)) } diff --git a/internal/cmd/beta/cdn/cdn.go b/internal/cmd/beta/cdn/cdn.go new file mode 100644 index 000000000..09d05af76 --- /dev/null +++ b/internal/cmd/beta/cdn/cdn.go @@ -0,0 +1,26 @@ +package cdn + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "cdn", + Short: "Manage CDN resources", + Long: "Manage the lifecycle of CDN resources.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(distribution.NewCommand(params)) +} diff --git a/internal/cmd/beta/cdn/distribution/create/create.go b/internal/cmd/beta/cdn/distribution/create/create.go new file mode 100644 index 000000000..457009b27 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/create/create.go @@ -0,0 +1,330 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + cdnUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + flagHTTP = "http" + flagHTTPOriginURL = "http-origin-url" + flagHTTPGeofencing = "http-geofencing" + flagHTTPOriginRequestHeaders = "http-origin-request-headers" + flagBucket = "bucket" + flagBucketURL = "bucket-url" + flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" //nolint:gosec // linter false positive + flagBucketPassword = "bucket-password" + flagBucketRegion = "bucket-region" + flagBlockedCountries = "blocked-countries" + flagBlockedIPs = "blocked-ips" + flagDefaultCacheDuration = "default-cache-duration" + flagLoki = "loki" + flagLokiUsername = "loki-username" + flagLokiPassword = "loki-password" + flagLokiPushURL = "loki-push-url" + flagMonthlyLimitBytes = "monthly-limit-bytes" + flagOptimizer = "optimizer" +) + +var flagRegion = flags.StringEnumSliceFlag( + "regions", + cdn.AllowedRegionEnumValues, + "Regions in which content should be cached, multiple values accepted,", +) + +type httpInputModel struct { + OriginURL string + Geofencing *map[string][]string + OriginRequestHeaders *map[string]string +} + +type bucketInputModel struct { + URL string + AccessKeyID string + Password string + Region string +} + +type lokiInputModel struct { + Username string + Password string + PushURL string +} + +type inputModel struct { + *globalflags.GlobalFlagModel + Regions []cdn.Region + HTTP *httpInputModel + Bucket *bucketInputModel + BlockedCountries []string + BlockedIPs []string + DefaultCacheDuration string + MonthlyLimitBytes *int64 + Loki *lokiInputModel + Optimizer bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a CDN distribution", + Long: "Create a CDN distribution for a given originUrl in multiple regions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a CDN distribution with an HTTP backend`, + `$ stackit beta cdn distribution create --http --http-origin-url https://example.com \ +--regions AF,EU`, + ), + examples.NewExample( + `Create a CDN distribution with an Object Storage backend`, + `$ stackit beta cdn distribution create --bucket --bucket-url https://bucket.example.com \ +--bucket-credentials-access-key-id yyyy --bucket-region EU \ +--regions AF,EU`, + ), + examples.NewExample( + `Create a CDN distribution passing the password via stdin, take care that there's a '\n' at the end of the input'`, + `$ cat secret.txt | stackit beta cdn distribution create -y --project-id xxx \ +--bucket --bucket-url https://bucket.example.com --bucekt-credentials-access-key-id yyyy --bucket-region EU \ +--regions AF,EU`, + ), + ), + PreRun: func(cmd *cobra.Command, _ []string) { + // either flagHTTP or flagBucket must be set, depending on which we mark other flags as required + if flags.FlagToBoolValue(params.Printer, cmd, flagHTTP) { + err := cmd.MarkFlagRequired(flagHTTPOriginURL) + cobra.CheckErr(err) + } else { + err := flags.MarkFlagsRequired(cmd, flagBucketURL, flagBucketCredentialsAccessKeyID, flagBucketRegion) + cobra.CheckErr(err) + } + // if user uses loki, mark related flags as required + if flags.FlagToBoolValue(params.Printer, cmd, flagLoki) { + err := flags.MarkFlagsRequired(cmd, flagLokiUsername, flagLokiPushURL) + cobra.CheckErr(err) + } + }, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a CDN distribution for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create CDN distribution: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd, params) + return cmd +} + +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { + flagRegion.Register(cmd) + cmd.Flags().Bool(flagHTTP, false, "Use HTTP backend") + cmd.Flags().String(flagHTTPOriginURL, "", "Origin URL for HTTP backend") + cmd.Flags().StringSlice(flagHTTPOriginRequestHeaders, []string{}, "Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!") + cmd.Flags().StringArray(flagHTTPGeofencing, []string{}, "Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable.") + cmd.Flags().Bool(flagBucket, false, "Use Object Storage backend") + cmd.Flags().String(flagBucketURL, "", "Bucket URL for Object Storage backend") + cmd.Flags().String(flagBucketCredentialsAccessKeyID, "", "Access Key ID for Object Storage backend") + bucketPassword := flags.SecretFlag(flagBucketPassword, params) + cmd.Flags().Var(bucketPassword, flagBucketPassword, bucketPassword.Usage()) + cmd.Flags().String(flagBucketRegion, "", "Region for Object Storage backend") + cmd.Flags().StringSlice(flagBlockedCountries, []string{}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')") + cmd.Flags().StringSlice(flagBlockedIPs, []string{}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')") + cmd.Flags().String(flagDefaultCacheDuration, "", "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)") + cmd.Flags().Bool(flagLoki, false, "Enable Loki log sink for the CDN distribution") + cmd.Flags().String(flagLokiUsername, "", "Username for log sink") + lokiPassword := flags.SecretFlag(flagLokiPassword, params) + cmd.Flags().Var(lokiPassword, flagLokiPassword, lokiPassword.Usage()) + cmd.Flags().String(flagLokiPushURL, "", "Push URL for log sink") + cmd.Flags().Int64(flagMonthlyLimitBytes, 0, "Monthly limit in bytes for the CDN distribution") + cmd.Flags().Bool(flagOptimizer, false, "Enable optimizer for the CDN distribution (paid feature).") + cmd.MarkFlagsMutuallyExclusive(flagHTTP, flagBucket) + cmd.MarkFlagsOneRequired(flagHTTP, flagBucket) + err := flags.MarkFlagsRequired(cmd, flagRegion.Name()) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + var http *httpInputModel + if flags.FlagToBoolValue(p, cmd, flagHTTP) { + originURL := flags.FlagToStringValue(p, cmd, flagHTTPOriginURL) + + var geofencing *map[string][]string + geofencingInput := flags.FlagToStringArrayValue(p, cmd, flagHTTPGeofencing) + if geofencingInput != nil { + geofencing = cdnUtils.ParseGeofencing(p, geofencingInput) + } + + var originRequestHeaders *map[string]string + originRequestHeadersInput := flags.FlagToStringSliceValue(p, cmd, flagHTTPOriginRequestHeaders) + if originRequestHeadersInput != nil { + originRequestHeaders = cdnUtils.ParseOriginRequestHeaders(p, originRequestHeadersInput) + } + + http = &httpInputModel{ + OriginURL: originURL, + Geofencing: geofencing, + OriginRequestHeaders: originRequestHeaders, + } + } + + var bucket *bucketInputModel + if flags.FlagToBoolValue(p, cmd, flagBucket) { + bucketURL := flags.FlagToStringValue(p, cmd, flagBucketURL) + accessKeyID := flags.FlagToStringValue(p, cmd, flagBucketCredentialsAccessKeyID) + region := flags.FlagToStringValue(p, cmd, flagBucketRegion) + password := flags.SecretFlagToString(p, cmd, flagBucketPassword) + + bucket = &bucketInputModel{ + URL: bucketURL, + AccessKeyID: accessKeyID, + Password: password, + Region: region, + } + } + + blockedCountries := flags.FlagToStringSliceValue(p, cmd, flagBlockedCountries) + blockedIPs := flags.FlagToStringSliceValue(p, cmd, flagBlockedIPs) + cacheDuration := flags.FlagToStringValue(p, cmd, flagDefaultCacheDuration) + monthlyLimit := flags.FlagToInt64Pointer(p, cmd, flagMonthlyLimitBytes) + + var loki *lokiInputModel + if flags.FlagToBoolValue(p, cmd, flagLoki) { + loki = &lokiInputModel{ + Username: flags.FlagToStringValue(p, cmd, flagLokiUsername), + PushURL: flags.FlagToStringValue(p, cmd, flagLokiPushURL), + Password: flags.SecretFlagToString(p, cmd, flagLokiPassword), + } + } + + optimizer := flags.FlagToBoolValue(p, cmd, flagOptimizer) + + model := inputModel{ + GlobalFlagModel: globalFlags, + Regions: flagRegion.Get(), + HTTP: http, + Bucket: bucket, + BlockedCountries: blockedCountries, + BlockedIPs: blockedIPs, + DefaultCacheDuration: cacheDuration, + MonthlyLimitBytes: monthlyLimit, + Loki: loki, + Optimizer: optimizer, + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiCreateDistributionRequest { + req := apiClient.DefaultAPI.CreateDistribution(ctx, model.ProjectId) + var backend cdn.CreateDistributionPayloadBackend + if model.HTTP != nil { + backend = cdn.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + Geofencing: model.HTTP.Geofencing, + OriginRequestHeaders: model.HTTP.OriginRequestHeaders, + OriginUrl: model.HTTP.OriginURL, + Type: "http", + }, + } + } else { + backend = cdn.CreateDistributionPayloadBackend{ + BucketBackendCreate: &cdn.BucketBackendCreate{ + BucketUrl: model.Bucket.URL, + Credentials: *cdn.NewBucketCredentials( + model.Bucket.AccessKeyID, + model.Bucket.Password, + ), + Region: model.Bucket.Region, + Type: "bucket", + }, + } + } + + payload := cdn.NewCreateDistributionPayload( + backend, + model.Regions, + ) + if len(model.BlockedCountries) > 0 { + payload.BlockedCountries = model.BlockedCountries + } + if len(model.BlockedIPs) > 0 { + payload.BlockedIps = model.BlockedIPs + } + if model.DefaultCacheDuration != "" { + payload.DefaultCacheDuration = utils.Ptr(model.DefaultCacheDuration) + } + if model.Loki != nil { + payload.LogSink = &cdn.LokiLogSinkCreate{ + Credentials: cdn.LokiLogSinkCredentials{ + Password: model.Loki.Password, + Username: model.Loki.Username, + }, + PushUrl: model.Loki.PushURL, + Type: "loki", + } + } + payload.MonthlyLimitBytes = model.MonthlyLimitBytes + if model.Optimizer { + payload.Optimizer = &cdn.Optimizer{ + Enabled: true, + } + } + return req.CreateDistributionPayload(*payload) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *cdn.CreateDistributionResponse) error { + if resp == nil { + return fmt.Errorf("create distribution response is nil") + } + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created CDN distribution for %q. ID: %s\n", projectLabel, resp.Distribution.Id) + return nil + }) +} diff --git a/internal/cmd/beta/cdn/distribution/create/create_test.go b/internal/cmd/beta/cdn/distribution/create/create_test.go new file mode 100644 index 000000000..966f74570 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/create/create_test.go @@ -0,0 +1,536 @@ +package create + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + "k8s.io/utils/ptr" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &cdn.APIClient{DefaultAPI: &cdn.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const testRegions = cdn.REGION_EU + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + flagRegion.Name(): string(testRegions), + } + flagsHTTPBackend()(flagValues) + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func flagsHTTPBackend() func(flagValues map[string]string) { + return func(flagValues map[string]string) { + delete(flagValues, flagBucket) + flagValues[flagHTTP] = "true" + flagValues[flagHTTPOriginURL] = "https://http-backend.example.com" + } +} + +func flagsBucketBackend() func(flagValues map[string]string) { + return func(flagValues map[string]string) { + delete(flagValues, flagHTTP) + flagValues[flagBucket] = "true" + flagValues[flagBucketURL] = "https://bucket-backend.example.com" + flagValues[flagBucketCredentialsAccessKeyID] = "access-key-id" + flagValues[flagBucketRegion] = "eu" + } +} + +func flagsLoki() func(flagValues map[string]string) { + return func(flagValues map[string]string) { + flagValues[flagLoki] = "true" + flagValues[flagLokiPushURL] = "https://loki.example.com" + flagValues[flagLokiUsername] = "loki-user" + } +} + +func flagRegions(regions ...cdn.Region) func(flagValues map[string]string) { + return func(flagValues map[string]string) { + if len(regions) == 0 { + delete(flagValues, flagRegion.Name()) + return + } + stringRegions := sdkUtils.EnumSliceToStringSlice(regions) + flagValues[flagRegion.Name()] = strings.Join(stringRegions, ",") + } +} + +func fixtureModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + Regions: []cdn.Region{testRegions}, + } + modelHTTPBackend()(model) + for _, mod := range mods { + mod(model) + } + return model +} + +func modelRegions(regions ...cdn.Region) func(model *inputModel) { + return func(model *inputModel) { + model.Regions = regions + } +} + +func modelHTTPBackend() func(model *inputModel) { + return func(model *inputModel) { + model.Bucket = nil + model.HTTP = &httpInputModel{ + OriginURL: "https://http-backend.example.com", + } + } +} + +func modelBucketBackend() func(model *inputModel) { + return func(model *inputModel) { + model.HTTP = nil + model.Bucket = &bucketInputModel{ + URL: "https://bucket-backend.example.com", + AccessKeyID: "access-key-id", + Region: "eu", + } + } +} + +func modelLoki() func(model *inputModel) { + return func(model *inputModel) { + model.Loki = &lokiInputModel{ + PushURL: "https://loki.example.com", + Username: "loki-user", + } + } +} + +func fixturePayload(mods ...func(payload *cdn.CreateDistributionPayload)) cdn.CreateDistributionPayload { + payload := *cdn.NewCreateDistributionPayload( + cdn.CreateDistributionPayloadBackend{ + HttpBackendCreate: &cdn.HttpBackendCreate{ + Type: "http", + OriginUrl: "https://http-backend.example.com", + }, + }, + []cdn.Region{testRegions}, + ) + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func payloadRegions(regions ...cdn.Region) func(payload *cdn.CreateDistributionPayload) { + return func(payload *cdn.CreateDistributionPayload) { + payload.Regions = regions + } +} + +func payloadBucketBackend() func(payload *cdn.CreateDistributionPayload) { + return func(payload *cdn.CreateDistributionPayload) { + payload.Backend = cdn.CreateDistributionPayloadBackend{ + BucketBackendCreate: &cdn.BucketBackendCreate{ + Type: "bucket", + BucketUrl: "https://bucket-backend.example.com", + Region: "eu", + Credentials: *cdn.NewBucketCredentials( + "access-key-id", + "", + ), + }, + } + } +} + +func payloadLoki() func(payload *cdn.CreateDistributionPayload) { + return func(payload *cdn.CreateDistributionPayload) { + payload.LogSink = &cdn.LokiLogSinkCreate{ + Type: "loki", + PushUrl: "https://loki.example.com", + Credentials: *cdn.NewLokiLogSinkCredentials("", "loki-user"), + } + } +} + +func fixtureRequest(mods ...func(payload *cdn.CreateDistributionPayload)) cdn.ApiCreateDistributionRequest { + req := testClient.DefaultAPI.CreateDistribution(testCtx, testProjectId) + req = req.CreateDistributionPayload(fixturePayload(mods...)) + return req +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expected: fixtureModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "regions missing", + flagValues: fixtureFlagValues(flagRegions()), + isValid: false, + }, + { + description: "multiple regions", + flagValues: fixtureFlagValues(flagRegions(cdn.REGION_EU, cdn.REGION_AF)), + isValid: true, + expected: fixtureModel(modelRegions(cdn.REGION_EU, cdn.REGION_AF)), + }, + { + description: "bucket backend", + flagValues: fixtureFlagValues(flagsBucketBackend()), + isValid: true, + expected: fixtureModel(modelBucketBackend()), + }, + { + description: "bucket backend missing url", + flagValues: fixtureFlagValues( + flagsBucketBackend(), + func(flagValues map[string]string) { + delete(flagValues, flagBucketURL) + }, + ), + isValid: false, + }, + { + description: "bucket backend missing access key id", + flagValues: fixtureFlagValues( + flagsBucketBackend(), + func(flagValues map[string]string) { + delete(flagValues, flagBucketCredentialsAccessKeyID) + }, + ), + isValid: false, + }, + { + description: "bucket backend missing region", + flagValues: fixtureFlagValues( + flagsBucketBackend(), + func(flagValues map[string]string) { + delete(flagValues, flagBucketRegion) + }, + ), + isValid: false, + }, + { + description: "http backend missing url", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + delete(flagValues, flagHTTPOriginURL) + }, + ), + isValid: false, + }, + { + description: "http backend with geofencing", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagHTTPGeofencing] = "https://dach.example.com DE,AT,CH" + }, + ), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.HTTP.Geofencing = &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + } + }, + ), + }, + { + description: "http backend with origin request headers", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagHTTPOriginRequestHeaders] = "X-Custom-Header:Value1,X-Another-Header:Value2" + }, + ), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.HTTP.OriginRequestHeaders = &map[string]string{ + "X-Custom-Header": "Value1", + "X-Another-Header": "Value2", + } + }, + ), + }, + { + description: "with blocked countries", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagBlockedCountries] = "DE,AT" + }), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.BlockedCountries = []string{"DE", "AT"} + }, + ), + }, + { + description: "with blocked IPs", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagBlockedIPs] = "127.0.0.1,10.0.0.8" + }), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.BlockedIPs = []string{"127.0.0.1", "10.0.0.8"} + }), + }, + { + description: "with default cache duration", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagDefaultCacheDuration] = "PT1H30M" + }), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.DefaultCacheDuration = "PT1H30M" + }), + }, + { + description: "with optimizer", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagOptimizer] = "true" + }), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.Optimizer = true + }), + }, + { + description: "with loki", + flagValues: fixtureFlagValues( + flagsLoki(), + ), + isValid: true, + expected: fixtureModel( + modelLoki(), + ), + }, + { + description: "loki with missing username", + flagValues: fixtureFlagValues( + flagsLoki(), + func(flagValues map[string]string) { + delete(flagValues, flagLokiUsername) + }, + ), + isValid: false, + }, + { + description: "loki with missing push url", + flagValues: fixtureFlagValues( + flagsLoki(), + func(flagValues map[string]string) { + delete(flagValues, flagLokiPushURL) + }, + ), + isValid: false, + }, + { + description: "with monthly limit bytes", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagMonthlyLimitBytes] = "1073741824" // 1 GiB + }), + isValid: true, + expected: fixtureModel( + func(model *inputModel) { + model.MonthlyLimitBytes = ptr.To[int64](1073741824) + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected cdn.ApiCreateDistributionRequest + }{ + { + description: "base", + model: fixtureModel(), + expected: fixtureRequest(), + }, + { + description: "multiple regions", + model: fixtureModel(modelRegions(cdn.REGION_AF, cdn.REGION_EU)), + expected: fixtureRequest(payloadRegions(cdn.REGION_AF, cdn.REGION_EU)), + }, + { + description: "bucket backend", + model: fixtureModel(modelBucketBackend()), + expected: fixtureRequest(payloadBucketBackend()), + }, + { + description: "http backend with geofencing and origin request headers", + model: fixtureModel( + func(model *inputModel) { + model.HTTP.Geofencing = &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + } + model.HTTP.OriginRequestHeaders = &map[string]string{ + "X-Custom-Header": "Value1", + "X-Another-Header": "Value2", + } + }, + ), + expected: fixtureRequest( + func(payload *cdn.CreateDistributionPayload) { + payload.Backend.HttpBackendCreate.Geofencing = &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + } + payload.Backend.HttpBackendCreate.OriginRequestHeaders = &map[string]string{ + "X-Custom-Header": "Value1", + "X-Another-Header": "Value2", + } + }, + ), + }, + { + description: "with full options", + model: fixtureModel( + func(model *inputModel) { + model.MonthlyLimitBytes = ptr.To[int64](5368709120) // 5 GiB + model.Optimizer = true + model.BlockedCountries = []string{"DE", "AT"} + model.BlockedIPs = []string{"127.0.0.1"} + model.DefaultCacheDuration = "PT2H" + }, + ), + expected: fixtureRequest( + func(payload *cdn.CreateDistributionPayload) { + payload.MonthlyLimitBytes = utils.Ptr[int64](5368709120) + payload.Optimizer = &cdn.Optimizer{ + Enabled: true, + } + payload.BlockedCountries = []string{"DE", "AT"} + payload.BlockedIps = []string{"127.0.0.1"} + payload.DefaultCacheDuration = utils.Ptr("PT2H") + }, + ), + }, + { + description: "loki", + model: fixtureModel( + modelLoki(), + ), + expected: fixtureRequest(payloadLoki()), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expected, + cmp.AllowUnexported(tt.expected, cdn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFormat string + response *cdn.CreateDistributionResponse + expected string + wantErr bool + }{ + { + description: "nil response", + outputFormat: "table", + response: nil, + wantErr: true, + }, + { + description: "table output", + outputFormat: "table", + response: &cdn.CreateDistributionResponse{ + Distribution: cdn.Distribution{ + Id: "dist-1234", + }, + }, + expected: fmt.Sprintf("Created CDN distribution for %q. ID: dist-1234\n", testProjectId), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, tt.outputFormat, testProjectId, tt.response); (err != nil) != tt.wantErr { + t.Fatalf("outputResult: %v", err) + } + if params.Out.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, params.Out.String()) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/delete/delete.go b/internal/cmd/beta/cdn/distribution/delete/delete.go new file mode 100644 index 000000000..ccce137c6 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/delete/delete.go @@ -0,0 +1,93 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const argDistributionID = "DISTRIBUTION_ID" + +type inputModel struct { + *globalflags.GlobalFlagModel + DistributionID string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a CDN distribution", + Long: "Delete a CDN distribution by its ID.", + Args: args.SingleArg(argDistributionID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a CDN distribution with ID "xxx"`, + `$ stackit beta cdn distribution delete xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the CDN distribution %q for project %q?", model.DistributionID, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete loadbalancer: %w", err) + } + + params.Printer.Outputf("CDN distribution %q deleted.\n", model.DistributionID) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + distributionID := inputArgs[0] + model := inputModel{ + GlobalFlagModel: globalFlags, + DistributionID: distributionID, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiDeleteDistributionRequest { + return apiClient.DefaultAPI.DeleteDistribution(ctx, model.ProjectId, model.DistributionID) +} diff --git a/internal/cmd/beta/cdn/distribution/delete/delete_test.go b/internal/cmd/beta/cdn/distribution/delete/delete_test.go new file mode 100644 index 000000000..00fe5b7fe --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/delete/delete_test.go @@ -0,0 +1,131 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testClient = &cdn.APIClient{DefaultAPI: &cdn.DefaultAPIService{}} + testDistributionID = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argVales []string)) []string { + argVales := []string{ + testDistributionID, + } + for _, m := range mods { + m(argVales) + } + return argVales +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + DistributionID: testDistributionID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *cdn.ApiDeleteDistributionRequest)) cdn.ApiDeleteDistributionRequest { + request := testClient.DefaultAPI.DeleteDistribution(testCtx, testProjectId, testDistributionID) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argsValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argsValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argsValues: []string{}, + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + }, + isValid: false, + }, + { + description: "no arg values", + argsValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult cdn.ApiDeleteDistributionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult, cdn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/describe/describe.go b/internal/cmd/beta/cdn/distribution/describe/describe.go new file mode 100644 index 000000000..951ec942c --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/describe/describe.go @@ -0,0 +1,220 @@ +package describe + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/spf13/cobra" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const distributionIDArg = "DISTRIBUTION_ID_ARG" +const flagWithWaf = "with-waf" + +type inputModel struct { + *globalflags.GlobalFlagModel + DistributionID string + WithWAF bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describe a CDN distribution", + Long: "Describe a CDN distribution by its ID.", + Args: args.SingleArg(distributionIDArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of a CDN distribution with ID "xxx"`, + `$ stackit beta cdn distribution describe xxx`, + ), + examples.NewExample( + `Get details of a CDN, including WAF details, for ID "xxx"`, + `$ stackit beta cdn distribution describe xxx --with-waf`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read distribution: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(flagWithWaf, false, "Include WAF details in the distribution description") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + DistributionID: inputArgs[0], + WithWAF: flags.FlagToBoolValue(p, cmd, flagWithWaf), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) cdn.ApiGetDistributionRequest { + return apiClient.DefaultAPI.GetDistribution(ctx, model.ProjectId, model.DistributionID).WithWafStatus(model.WithWAF) +} + +func outputResult(p *print.Printer, outputFormat string, distribution *cdn.GetDistributionResponse) error { + if distribution == nil { + return fmt.Errorf("distribution response is empty") + } + return p.OutputResult(outputFormat, distribution, func() error { + d := distribution.Distribution + var content []tables.Table + + content = append(content, buildDistributionTable(&d)) + + if d.Waf != nil { + content = append(content, buildWAFTable(&d)) + } + + err := tables.DisplayTables(p, content) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} + +func buildDistributionTable(d *cdn.Distribution) tables.Table { + regions := strings.Join(sdkUtils.EnumSliceToStringSlice(d.Config.Regions), ", ") + defaultCacheDuration := "" + if d.Config.DefaultCacheDuration.IsSet() && d.Config.DefaultCacheDuration.Get() != nil { + defaultCacheDuration = *d.Config.DefaultCacheDuration.Get() + } + logSinkPushUrl := "" + if d.Config.LogSink != nil && d.Config.LogSink.PushUrl != "" { + logSinkPushUrl = d.Config.LogSink.PushUrl + } + monthlyLimitBytes := "" + if d.Config.MonthlyLimitBytes.IsSet() && d.Config.MonthlyLimitBytes.Get() != nil { + monthlyLimitBytes = fmt.Sprintf("%d", *d.Config.MonthlyLimitBytes.Get()) + } + optimizerEnabled := "" + if d.Config.Optimizer != nil { + optimizerEnabled = fmt.Sprintf("%t", d.Config.Optimizer.Enabled) + } + table := tables.NewTable() + table.SetTitle("Distribution") + table.AddRow("ID", d.Id) + table.AddSeparator() + table.AddRow("STATUS", d.Status) + table.AddSeparator() + table.AddRow("REGIONS", regions) + table.AddSeparator() + table.AddRow("CREATED AT", d.CreatedAt) + table.AddSeparator() + table.AddRow("UPDATED AT", d.UpdatedAt) + table.AddSeparator() + table.AddRow("PROJECT ID", d.ProjectId) + table.AddSeparator() + if len(d.Errors) > 0 { + var errorDescriptions []string + for _, err := range d.Errors { + errorDescriptions = append(errorDescriptions, err.En) + } + table.AddRow("ERRORS", strings.Join(errorDescriptions, "\n")) + table.AddSeparator() + } + if d.Config.Backend.BucketBackend != nil { + b := d.Config.Backend.BucketBackend + table.AddRow("BACKEND TYPE", "BUCKET") + table.AddSeparator() + table.AddRow("BUCKET URL", b.BucketUrl) + table.AddSeparator() + table.AddRow("BUCKET REGION", b.Region) + table.AddSeparator() + } else if d.Config.Backend.HttpBackend != nil { + h := d.Config.Backend.HttpBackend + var geofencing []string + if h.Geofencing != nil { + for k, v := range h.Geofencing { + geofencing = append(geofencing, fmt.Sprintf("%s: %s", k, strings.Join(v, ", "))) + } + } + slices.Sort(geofencing) + table.AddRow("BACKEND TYPE", "HTTP") + table.AddSeparator() + table.AddRow("HTTP ORIGIN URL", h.OriginUrl) + table.AddSeparator() + if h.OriginRequestHeaders != nil { + table.AddRow("HTTP ORIGIN REQUEST HEADERS", utils.JoinStringMap(h.OriginRequestHeaders, ": ", ", ")) + table.AddSeparator() + } + table.AddRow("HTTP GEOFENCING PROPERTIES", strings.Join(geofencing, "\n")) + table.AddSeparator() + } + table.AddRow("BLOCKED COUNTRIES", strings.Join(d.Config.BlockedCountries, ", ")) + table.AddSeparator() + table.AddRow("BLOCKED IPS", strings.Join(d.Config.BlockedIps, ", ")) + table.AddSeparator() + table.AddRow("DEFAULT CACHE DURATION", defaultCacheDuration) + table.AddSeparator() + table.AddRow("LOG SINK PUSH URL", logSinkPushUrl) + table.AddSeparator() + table.AddRow("MONTHLY LIMIT (BYTES)", monthlyLimitBytes) + table.AddSeparator() + table.AddRow("OPTIMIZER ENABLED", optimizerEnabled) + table.AddSeparator() + return table +} + +func buildWAFTable(d *cdn.Distribution) tables.Table { + table := tables.NewTable() + table.SetTitle("WAF") + for _, disabled := range d.Waf.DisabledRules { + table.AddRow("DISABLED RULE ID", disabled.Id) + table.AddSeparator() + } + for _, enabled := range d.Waf.EnabledRules { + table.AddRow("ENABLED RULE ID", enabled.Id) + table.AddSeparator() + } + for _, logOnly := range d.Waf.LogOnlyRules { + table.AddRow("LOG-ONLY RULE ID", logOnly.Id) + table.AddSeparator() + } + return table +} diff --git a/internal/cmd/beta/cdn/distribution/describe/describe_test.go b/internal/cmd/beta/cdn/distribution/describe/describe_test.go new file mode 100644 index 000000000..61e3fc270 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/describe/describe_test.go @@ -0,0 +1,407 @@ +package describe + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectID = uuid.NewString() + testDistributionID = uuid.NewString() + testClient = &cdn.APIClient{DefaultAPI: &cdn.DefaultAPIService{}} + testTime = time.Time{} +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectID, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectID, + Verbosity: globalflags.VerbosityDefault, + }, + DistributionID: testDistributionID, + WithWAF: false, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureResponse(mods ...func(resp *cdn.GetDistributionResponse)) *cdn.GetDistributionResponse { + response := &cdn.GetDistributionResponse{ + Distribution: cdn.Distribution{ + Config: cdn.Config{ + Backend: cdn.ConfigBackend{ + BucketBackend: &cdn.BucketBackend{ + BucketUrl: "https://example.com", + Region: "eu", + Type: "bucket", + }, + }, + BlockedCountries: []string{}, + BlockedIps: []string{}, + DefaultCacheDuration: *cdn.NewNullableString(nil), + LogSink: nil, + MonthlyLimitBytes: *cdn.NewNullableInt64(nil), + Optimizer: nil, + Regions: []cdn.Region{cdn.REGION_EU}, + Waf: cdn.WafConfig{}, + }, + CreatedAt: testTime, + Domains: []cdn.Domain{}, + Errors: nil, + Id: testDistributionID, + ProjectId: testProjectID, + Status: wait.DISTRIBUTIONSTATUS_ACTIVE, + UpdatedAt: testTime, + Waf: nil, + }, + } + for _, mod := range mods { + mod(response) + } + return response +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + args []string + flags map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + args: []string{testDistributionID}, + flags: fixtureFlagValues(), + isValid: true, + expected: fixtureInputModel(), + }, + { + description: "no args", + args: []string{}, + flags: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid distribution id", + args: []string{"invalid-uuid"}, + flags: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing project id", + args: []string{testDistributionID}, + flags: map[string]string{}, + isValid: false, + }, + { + description: "invalid project id", + args: []string{testDistributionID}, + flags: map[string]string{ + globalflags.ProjectIdFlag: "invalid-uuid", + }, + isValid: false, + }, + { + description: "with WAF", + args: []string{testDistributionID}, + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[flagWithWaf] = "true" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.WithWAF = true + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.args, tt.flags, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected cdn.ApiGetDistributionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expected: testClient.DefaultAPI.GetDistribution(testCtx, testProjectID, testDistributionID).WithWafStatus(false), + }, + { + description: "with WAF", + model: fixtureInputModel(func(model *inputModel) { + model.WithWAF = true + }), + expected: testClient.DefaultAPI.GetDistribution(testCtx, testProjectID, testDistributionID).WithWafStatus(true), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(got, tt.expected, + cmp.AllowUnexported(tt.expected, cdn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + format string + distribution *cdn.GetDistributionResponse + wantErr bool + expected string + }{ + { + description: "empty", + format: "table", + wantErr: true, + }, + { + description: "no errors", + format: "table", + distribution: fixtureResponse(), + //nolint:staticcheck //you can't use escape sequences in ``-string-literals + expected: fmt.Sprintf(` + Distribution  + ID │ %-37s +────────────────────────┼────────────────────────────────────── + STATUS │ ACTIVE +────────────────────────┼────────────────────────────────────── + REGIONS │ EU +────────────────────────┼────────────────────────────────────── + CREATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + UPDATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + PROJECT ID │ %-37s +────────────────────────┼────────────────────────────────────── + BACKEND TYPE │ BUCKET +────────────────────────┼────────────────────────────────────── + BUCKET URL │ https://example.com +────────────────────────┼────────────────────────────────────── + BUCKET REGION │ eu +────────────────────────┼────────────────────────────────────── + BLOCKED COUNTRIES │ +────────────────────────┼────────────────────────────────────── + BLOCKED IPS │ +────────────────────────┼────────────────────────────────────── + DEFAULT CACHE DURATION │ +────────────────────────┼────────────────────────────────────── + LOG SINK PUSH URL │ +────────────────────────┼────────────────────────────────────── + MONTHLY LIMIT (BYTES) │ +────────────────────────┼────────────────────────────────────── + OPTIMIZER ENABLED │ + +`, + testDistributionID, + testTime, + testTime, + testProjectID), + }, + { + description: "with errors", + format: "table", + distribution: fixtureResponse( + func(r *cdn.GetDistributionResponse) { + r.Distribution.Errors = []cdn.StatusError{ + { + En: "First error message", + }, + { + En: "Second error message", + }, + } + }, + ), + //nolint:staticcheck //you can't use escape sequences in ``-string-literals + expected: fmt.Sprintf(` + Distribution  + ID │ %-37s +────────────────────────┼────────────────────────────────────── + STATUS │ ACTIVE +────────────────────────┼────────────────────────────────────── + REGIONS │ EU +────────────────────────┼────────────────────────────────────── + CREATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + UPDATED AT │ %-37s +────────────────────────┼────────────────────────────────────── + PROJECT ID │ %-37s +────────────────────────┼────────────────────────────────────── + ERRORS │ First error message + │ Second error message +────────────────────────┼────────────────────────────────────── + BACKEND TYPE │ BUCKET +────────────────────────┼────────────────────────────────────── + BUCKET URL │ https://example.com +────────────────────────┼────────────────────────────────────── + BUCKET REGION │ eu +────────────────────────┼────────────────────────────────────── + BLOCKED COUNTRIES │ +────────────────────────┼────────────────────────────────────── + BLOCKED IPS │ +────────────────────────┼────────────────────────────────────── + DEFAULT CACHE DURATION │ +────────────────────────┼────────────────────────────────────── + LOG SINK PUSH URL │ +────────────────────────┼────────────────────────────────────── + MONTHLY LIMIT (BYTES) │ +────────────────────────┼────────────────────────────────────── + OPTIMIZER ENABLED │ + +`, testDistributionID, + testTime, + testTime, + testProjectID), + }, + { + description: "full", + format: "table", + distribution: fixtureResponse( + func(r *cdn.GetDistributionResponse) { + r.Distribution.Waf = &cdn.DistributionWaf{ + EnabledRules: []cdn.WafStatusRuleBlock{ + {Id: "rule-id-1"}, + {Id: "rule-id-2"}, + }, + DisabledRules: []cdn.WafStatusRuleBlock{ + {Id: "rule-id-3"}, + {Id: "rule-id-4"}, + }, + LogOnlyRules: []cdn.WafStatusRuleBlock{ + {Id: "rule-id-5"}, + {Id: "rule-id-6"}, + }, + } + r.Distribution.Config.Backend = cdn.ConfigBackend{ + HttpBackend: &cdn.HttpBackend{ + OriginUrl: "https://origin.example.com", + OriginRequestHeaders: map[string]string{ + "X-Custom-Header": "CustomValue", + }, + Geofencing: map[string][]string{ + "origin1.example.com": {"US", "CA"}, + "origin2.example.com": {"FR", "DE"}, + }, + }, + } + r.Distribution.Config.BlockedCountries = []string{"US", "CN"} + r.Distribution.Config.BlockedIps = []string{"127.0.0.1"} + r.Distribution.Config.DefaultCacheDuration = *cdn.NewNullableString(utils.Ptr("P1DT2H30M")) + r.Distribution.Config.LogSink = &cdn.LokiLogSink{ + PushUrl: "https://logs.example.com", + } + monthlyLimit := int64(104857600) + r.Distribution.Config.MonthlyLimitBytes = *cdn.NewNullableInt64(&monthlyLimit) + r.Distribution.Config.Optimizer = &cdn.Optimizer{ + Enabled: true, + } + }), + //nolint:staticcheck //you can't use escape sequences in ``-string-literals + expected: fmt.Sprintf(` + Distribution  + ID │ %-37s +─────────────────────────────┼────────────────────────────────────── + STATUS │ ACTIVE +─────────────────────────────┼────────────────────────────────────── + REGIONS │ EU +─────────────────────────────┼────────────────────────────────────── + CREATED AT │ %-37s +─────────────────────────────┼────────────────────────────────────── + UPDATED AT │ %-37s +─────────────────────────────┼────────────────────────────────────── + PROJECT ID │ %-37s +─────────────────────────────┼────────────────────────────────────── + BACKEND TYPE │ HTTP +─────────────────────────────┼────────────────────────────────────── + HTTP ORIGIN URL │ https://origin.example.com +─────────────────────────────┼────────────────────────────────────── + HTTP ORIGIN REQUEST HEADERS │ X-Custom-Header: CustomValue +─────────────────────────────┼────────────────────────────────────── + HTTP GEOFENCING PROPERTIES │ origin1.example.com: US, CA + │ origin2.example.com: FR, DE +─────────────────────────────┼────────────────────────────────────── + BLOCKED COUNTRIES │ US, CN +─────────────────────────────┼────────────────────────────────────── + BLOCKED IPS │ 127.0.0.1 +─────────────────────────────┼────────────────────────────────────── + DEFAULT CACHE DURATION │ P1DT2H30M +─────────────────────────────┼────────────────────────────────────── + LOG SINK PUSH URL │ https://logs.example.com +─────────────────────────────┼────────────────────────────────────── + MONTHLY LIMIT (BYTES) │ 104857600 +─────────────────────────────┼────────────────────────────────────── + OPTIMIZER ENABLED │ true + + + WAF  + DISABLED RULE ID │ rule-id-3 +──────────────────┼─────────── + DISABLED RULE ID │ rule-id-4 +──────────────────┼─────────── + ENABLED RULE ID │ rule-id-1 +──────────────────┼─────────── + ENABLED RULE ID │ rule-id-2 +──────────────────┼─────────── + LOG-ONLY RULE ID │ rule-id-5 +──────────────────┼─────────── + LOG-ONLY RULE ID │ rule-id-6 + +`, testDistributionID, testTime, testTime, testProjectID), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, tt.format, tt.distribution); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(params.Out.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/distribution.go b/internal/cmd/beta/cdn/distribution/distribution.go new file mode 100644 index 000000000..c6cb8e018 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/distribution.go @@ -0,0 +1,33 @@ +package distribution + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/cdn/distribution/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCommand(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "distribution", + Short: "Manage CDN distributions", + Long: "Manage the lifecycle of CDN distributions.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) +} diff --git a/internal/cmd/beta/cdn/distribution/list/list.go b/internal/cmd/beta/cdn/distribution/list/list.go new file mode 100644 index 000000000..1a47a908f --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/list/list.go @@ -0,0 +1,182 @@ +package list + +import ( + "context" + "fmt" + "math" + "strings" + + "github.com/spf13/cobra" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + SortBy string + Limit *int32 +} + +const ( + limitFlag = "" + maxPageSize = int32(100) +) + +var sortByFlag = flags.StringEnumFlag( + "sort-by", + []string{"id", "createdAt", "updatedAt", "originUrl", "status", "originUrlRelated"}, + "Sort entries by a specific field,", + flags.StringEnumDefaultValue("createdAt"), +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List CDN distributions", + Long: "List all CDN distributions in your account.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all CDN distributions`, + `$ stackit beta cdn distribution list`, + ), + examples.NewExample( + `List all CDN distributions sorted by id`, + `$ stackit beta cdn distribution list --sort-by=id`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + distributions, err := fetchDistributions(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("fetch distributions: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, distributions) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + // same default as apiClient + sortByFlag.Register(cmd.Flags()) + cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt32Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SortBy: sortByFlag.Get(), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *cdn.APIClient, nextPageID string, pageLimit int32) cdn.ApiListDistributionsRequest { + req := apiClient.DefaultAPI.ListDistributions(ctx, model.ProjectId) + req = req.SortBy(model.SortBy) + req = req.PageSize(pageLimit) + if nextPageID != "" { + req = req.PageIdentifier(nextPageID) + } + return req +} + +func outputResult(p *print.Printer, outputFormat string, distributions []cdn.Distribution) error { + if distributions == nil { + distributions = make([]cdn.Distribution, 0) // otherwise prints null in json output + } + return p.OutputResult(outputFormat, distributions, func() error { + if len(distributions) == 0 { + p.Outputln("No CDN distributions found") + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "REGIONS", "STATUS") + for _, d := range distributions { + var joinedRegions string + if d.Config.Regions != nil { + joinedRegions = strings.Join(sdkUtils.EnumSliceToStringSlice(d.Config.Regions), ", ") + } + table.AddRow( + d.Id, + joinedRegions, + d.Status, + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} + +func fetchDistributions(ctx context.Context, model *inputModel, apiClient *cdn.APIClient) ([]cdn.Distribution, error) { + var nextPageID string + var distributions []cdn.Distribution + received := int32(0) + limit := int32(math.MaxInt32) + if model.Limit != nil { + limit = min(limit, *model.Limit) + } + for { + want := min(maxPageSize, limit-received) + request := buildRequest(ctx, model, apiClient, nextPageID, want) + response, err := request.Execute() + if err != nil { + return nil, fmt.Errorf("list distributions: %w", err) + } + if response.Distributions != nil { + distributions = append(distributions, response.Distributions...) + } + nextPageID = "" + if response.NextPageIdentifier != nil { + nextPageID = *response.NextPageIdentifier + } + received += want + if nextPageID == "" || received >= limit { + break + } + } + return distributions, nil +} diff --git a/internal/cmd/beta/cdn/distribution/list/list_test.go b/internal/cmd/beta/cdn/distribution/list/list_test.go new file mode 100644 index 000000000..26c21f017 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/list/list_test.go @@ -0,0 +1,484 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "slices" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testProjectId = uuid.NewString() +var testClient = &cdn.APIClient{DefaultAPI: &cdn.DefaultAPIService{}} +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + +const ( + testNextPageID = "next-page-id-123" + testID = "dist-1" + testStatus = wait.DISTRIBUTIONSTATUS_ACTIVE +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + m := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + SortBy: "createdAt", + } + for _, mod := range mods { + mod(m) + } + return m +} + +func fixtureRequest(mods ...func(request cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + request := testClient.DefaultAPI.ListDistributions(testCtx, testProjectId) + request = request.PageSize(100) + request = request.SortBy("createdAt") + for _, mod := range mods { + request = mod(request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expected: fixtureInputModel(), + }, + { + description: "no project id", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "sort by id", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "id" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "id" + }), + }, + { + description: "sort by origin-url", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "originUrl" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "originUrl" + }), + }, + { + description: "sort by status", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "status" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "status" + }), + }, + { + description: "sort by created", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "createdAt" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "createdAt" + }), + }, + { + description: "sort by updated", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "updatedAt" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "updatedAt" + }), + }, + { + description: "sort by originUrlRelated", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "originUrlRelated" + }), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "originUrlRelated" + }), + }, + { + description: "invalid sort by", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[sortByFlag.Name()] = "invalid" + }), + isValid: false, + }, + { + description: "missing sort by uses default", + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + delete(flagValues, sortByFlag.Name()) + }, + ), + isValid: true, + expected: fixtureInputModel(func(model *inputModel) { + model.SortBy = "createdAt" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + nextPageID string + expected cdn.ApiListDistributionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expected: fixtureRequest(), + }, + { + description: "sort by updatedAt", + inputModel: fixtureInputModel(func(model *inputModel) { + model.SortBy = "updatedAt" + }), + expected: fixtureRequest(func(req cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + return req.SortBy("updatedAt") + }), + }, + { + description: "with next page id", + inputModel: fixtureInputModel(), + nextPageID: testNextPageID, + expected: fixtureRequest(func(req cdn.ApiListDistributionsRequest) cdn.ApiListDistributionsRequest { + return req.PageIdentifier(testNextPageID) + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + req := buildRequest(testCtx, tt.inputModel, testClient, tt.nextPageID, maxPageSize) + diff := cmp.Diff(req, tt.expected, + cmp.AllowUnexported(tt.expected, cdn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Errorf("buildRequest() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +type testResponse struct { + statusCode int + body cdn.ListDistributionsResponse +} + +func fixtureTestResponse(mods ...func(resp *testResponse)) testResponse { + resp := testResponse{ + statusCode: 200, + } + for _, mod := range mods { + mod(&resp) + } + return resp +} + +func fixtureDistributions(count int) []cdn.Distribution { + distributions := make([]cdn.Distribution, count) + for i := 0; i < count; i++ { + id := fmt.Sprintf("dist-%d", i+1) + distributions[i] = cdn.Distribution{ + Id: id, + ProjectId: testProjectId, + Status: testStatus, + Config: cdn.Config{ + Backend: cdn.HttpBackendAsConfigBackend(&cdn.HttpBackend{ + Type: "http", + OriginUrl: "https://example.com", + AdditionalProperties: map[string]interface{}{}, + }), + Waf: cdn.WafConfig{ + Mode: "unknown_default_open_api", + Type: "unknown_default_open_api", + AdditionalProperties: map[string]interface{}{}, + }, + Tls: cdn.TlsConfig{ + AdditionalProperties: map[string]interface{}{}, + }, + Regions: []cdn.Region{cdn.REGION_EU}, + BlockedCountries: []string{}, + BlockedIps: []string{}, + AdditionalProperties: map[string]interface{}{}, + }, + Domains: []cdn.Domain{}, + AdditionalProperties: map[string]interface{}{}, + } + } + return distributions +} + +func TestFetchDistributions(t *testing.T) { + tests := []struct { + description string + limit int32 + responses []testResponse + expected []cdn.Distribution + fails bool + }{ + { + description: "no distributions", + responses: []testResponse{ + fixtureTestResponse(), + }, + expected: nil, + }, + + { + description: "single distribution, single page", + responses: []testResponse{ + fixtureTestResponse( + func(resp *testResponse) { + resp.body.Distributions = fixtureDistributions(1) + }, + ), + }, + expected: fixtureDistributions(1), + }, + { + description: "multiple distributions, multiple pages", + responses: []testResponse{ + fixtureTestResponse( + func(resp *testResponse) { + resp.body.NextPageIdentifier = utils.Ptr(testNextPageID) + resp.body.Distributions = fixtureDistributions(1) + }, + ), + fixtureTestResponse( + func(resp *testResponse) { + resp.body.Distributions = fixtureDistributions(2)[1:] + }, + ), + }, + expected: fixtureDistributions(2), + }, + { + description: "API error", + responses: []testResponse{ + fixtureTestResponse( + func(resp *testResponse) { + resp.statusCode = 500 + }, + ), + }, + fails: true, + }, + { + description: "API error on second page", + responses: []testResponse{ + fixtureTestResponse( + func(resp *testResponse) { + resp.body.NextPageIdentifier = utils.Ptr(testNextPageID) + resp.body.Distributions = fixtureDistributions(1) + }, + ), + fixtureTestResponse( + func(resp *testResponse) { + resp.statusCode = 500 + }, + ), + }, + fails: true, + }, + { + description: "limit across 2 pages", + limit: 110, + responses: []testResponse{ + fixtureTestResponse( + func(resp *testResponse) { + resp.body.NextPageIdentifier = utils.Ptr(testNextPageID) + distributions := fixtureDistributions(100) + resp.body.Distributions = distributions + }, + ), + fixtureTestResponse( + func(resp *testResponse) { + distributions := fixtureDistributions(10) + resp.body.Distributions = distributions + }, + ), + }, + expected: slices.Concat(fixtureDistributions(100), fixtureDistributions(10)), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + callCount := 0 + handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + resp := tt.responses[callCount] + callCount++ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.statusCode) + bs, err := json.Marshal(resp.body) + if err != nil { + t.Fatalf("marshal: %v", err) + } + _, err = w.Write(bs) + if err != nil { + t.Fatalf("write: %v", err) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + client, err := cdn.NewAPIClient( + sdkConfig.WithEndpoint(server.URL), + sdkConfig.WithoutAuthentication(), + ) + if err != nil { + t.Fatalf("failed to create test client: %v", err) + } + var mods []func(m *inputModel) + if tt.limit > 0 { + mods = append(mods, func(m *inputModel) { + m.Limit = utils.Ptr(tt.limit) + }) + } + model := fixtureInputModel(mods...) + got, err := fetchDistributions(testCtx, model, client) + if err != nil { + if !tt.fails { + t.Fatalf("fetchDistributions() unexpected error: %v", err) + } + return + } + if callCount != len(tt.responses) { + t.Errorf("fetchDistributions() expected %d calls, got %d", len(tt.responses), callCount) + } + diff := cmp.Diff(got, tt.expected, + cmpopts.EquateComparable( + cdn.NullableString{}, + cdn.NullableInt64{}, + )) + if diff != "" { + t.Errorf("fetchDistributions() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFormat string + distributions []cdn.Distribution + expected string + }{ + { + description: "no distributions", + outputFormat: "json", + distributions: []cdn.Distribution{}, + expected: `[] + +`, + }, + { + description: "no distributions nil slice", + outputFormat: "json", + expected: `[] + +`, + }, + { + description: "single distribution", + outputFormat: "table", + distributions: []cdn.Distribution{ + { + Id: testID, + Config: cdn.Config{ + Regions: []cdn.Region{ + cdn.REGION_EU, + cdn.REGION_AF, + }, + }, + Status: testStatus, + }, + }, + expected: ` + ID │ REGIONS │ STATUS +────────┼─────────┼──────── + dist-1 │ EU, AF │ ACTIVE + +`, + }, + { + description: "no distributions, table format", + outputFormat: "table", + expected: "No CDN distributions found\n", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, tt.outputFormat, tt.distributions); err != nil { + t.Fatalf("outputResult: %v", err) + } + if params.Out.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, params.Out.String()) + } + }) + } +} diff --git a/internal/cmd/beta/cdn/distribution/update/update.go b/internal/cmd/beta/cdn/distribution/update/update.go new file mode 100644 index 000000000..b4418dc50 --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/update/update.go @@ -0,0 +1,329 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/client" + cdnUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/cdn/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + argDistributionID = "DISTRIBUTION_ID" + flagHTTP = "http" + flagHTTPOriginURL = "http-origin-url" + flagHTTPGeofencing = "http-geofencing" + flagHTTPOriginRequestHeaders = "http-origin-request-headers" + flagBucket = "bucket" + flagBucketURL = "bucket-url" + flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" //nolint:gosec // linter false positive + flagBucketPassword = "bucket-password" + flagBucketRegion = "bucket-region" + flagBlockedCountries = "blocked-countries" + flagBlockedIPs = "blocked-ips" + flagDefaultCacheDuration = "default-cache-duration" + flagLoki = "loki" + flagLokiUsername = "loki-username" + flagLokiPassword = "loki-password" + flagLokiPushURL = "loki-push-url" + flagMonthlyLimitBytes = "monthly-limit-bytes" + flagOptimizer = "optimizer" +) + +var flagRegions = flags.StringEnumSliceFlag( + "regions", + cdn.AllowedRegionEnumValues, + "Regions in which content should be cached,", +) + +type bucketInputModel struct { + URL string + AccessKeyID string + Password string + Region string +} + +type httpInputModel struct { + Geofencing *map[string][]string + OriginRequestHeaders *map[string]string + OriginURL string +} + +type lokiInputModel struct { + Password string + Username string + PushURL string +} + +type inputModel struct { + *globalflags.GlobalFlagModel + DistributionID string + Regions []cdn.Region + Bucket *bucketInputModel + HTTP *httpInputModel + BlockedCountries []string + BlockedIPs []string + DefaultCacheDuration string + MonthlyLimitBytes *int64 + Loki *lokiInputModel + Optimizer *bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Update a CDN distribution", + Long: "Update a CDN distribution by its ID, allowing replacement of its regions.", + Args: args.SingleArg(argDistributionID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `update a CDN distribution with ID "xxx" to not use optimizer`, + `$ stackit beta cdn distribution update xxx --optimizer=false`, + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to update a CDN distribution for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update CDN distribution: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd, params) + return cmd +} + +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { + flagRegions.Register(cmd) + cmd.Flags().Bool(flagHTTP, false, "Use HTTP backend") + cmd.Flags().String(flagHTTPOriginURL, "", "Origin URL for HTTP backend") + cmd.Flags().StringSlice(flagHTTPOriginRequestHeaders, []string{}, "Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!") + cmd.Flags().StringArray(flagHTTPGeofencing, []string{}, "Geofencing rules for HTTP backend in the format 'https://example.com US,DE'. URL and countries have to be quoted. Repeatable.") + cmd.Flags().Bool(flagBucket, false, "Use Object Storage backend") + cmd.Flags().String(flagBucketURL, "", "Bucket URL for Object Storage backend") + cmd.Flags().String(flagBucketCredentialsAccessKeyID, "", "Access Key ID for Object Storage backend") + bucketPassword := flags.SecretFlag(flagBucketPassword, params) + cmd.Flags().Var(bucketPassword, flagBucketPassword, bucketPassword.Usage()) + cmd.Flags().String(flagBucketRegion, "", "Region for Object Storage backend") + cmd.Flags().StringSlice(flagBlockedCountries, []string{}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')") + cmd.Flags().StringSlice(flagBlockedIPs, []string{}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')") + cmd.Flags().String(flagDefaultCacheDuration, "", "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)") + cmd.Flags().Bool(flagLoki, false, "Enable Loki log sink for the CDN distribution") + cmd.Flags().String(flagLokiUsername, "", "Username for log sink") + lokiPassword := flags.SecretFlag(flagLokiPassword, params) + cmd.Flags().Var(lokiPassword, flagLokiPassword, lokiPassword.Usage()) + cmd.Flags().String(flagLokiPushURL, "", "Push URL for log sink") + cmd.Flags().Int64(flagMonthlyLimitBytes, 0, "Monthly limit in bytes for the CDN distribution") + cmd.Flags().Bool(flagOptimizer, false, "Enable optimizer for the CDN distribution (paid feature).") + cmd.MarkFlagsMutuallyExclusive(flagHTTP, flagBucket) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + distributionID := inputArgs[0] + + regions := flagRegions.Get() + + var http *httpInputModel + if flags.FlagToBoolValue(p, cmd, flagHTTP) { + originURL := flags.FlagToStringValue(p, cmd, flagHTTPOriginURL) + + var geofencing *map[string][]string + geofencingInput := flags.FlagToStringArrayValue(p, cmd, flagHTTPGeofencing) + if geofencingInput != nil { + geofencing = cdnUtils.ParseGeofencing(p, geofencingInput) + } + + var originRequestHeaders *map[string]string + originRequestHeadersInput := flags.FlagToStringSliceValue(p, cmd, flagHTTPOriginRequestHeaders) + if originRequestHeadersInput != nil { + originRequestHeaders = cdnUtils.ParseOriginRequestHeaders(p, originRequestHeadersInput) + } + + http = &httpInputModel{ + OriginURL: originURL, + Geofencing: geofencing, + OriginRequestHeaders: originRequestHeaders, + } + } + + var bucket *bucketInputModel + if flags.FlagToBoolValue(p, cmd, flagBucket) { + bucketURL := flags.FlagToStringValue(p, cmd, flagBucketURL) + accessKeyID := flags.FlagToStringValue(p, cmd, flagBucketCredentialsAccessKeyID) + region := flags.FlagToStringValue(p, cmd, flagBucketRegion) + password := flags.SecretFlagToString(p, cmd, flagBucketPassword) + + bucket = &bucketInputModel{ + URL: bucketURL, + AccessKeyID: accessKeyID, + Password: password, + Region: region, + } + } + + blockedCountries := flags.FlagToStringSliceValue(p, cmd, flagBlockedCountries) + blockedIPs := flags.FlagToStringSliceValue(p, cmd, flagBlockedIPs) + cacheDuration := flags.FlagToStringValue(p, cmd, flagDefaultCacheDuration) + monthlyLimit := flags.FlagToInt64Pointer(p, cmd, flagMonthlyLimitBytes) + + var loki *lokiInputModel + if flags.FlagToBoolValue(p, cmd, flagLoki) { + loki = &lokiInputModel{ + Username: flags.FlagToStringValue(p, cmd, flagLokiUsername), + PushURL: flags.FlagToStringValue(p, cmd, flagLokiPushURL), + Password: flags.SecretFlagToString(p, cmd, flagLokiPassword), + } + } + + var optimizer *bool + if cmd.Flags().Changed(flagOptimizer) { + o := flags.FlagToBoolValue(p, cmd, flagOptimizer) + optimizer = &o + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DistributionID: distributionID, + Regions: regions, + HTTP: http, + Bucket: bucket, + BlockedCountries: blockedCountries, + BlockedIPs: blockedIPs, + DefaultCacheDuration: cacheDuration, + MonthlyLimitBytes: monthlyLimit, + Loki: loki, + Optimizer: optimizer, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *cdn.APIClient, model *inputModel) cdn.ApiPatchDistributionRequest { + req := apiClient.DefaultAPI.PatchDistribution(ctx, model.ProjectId, model.DistributionID) + payload := cdn.NewPatchDistributionPayload() + cfg := &cdn.ConfigPatch{} + payload.Config = cfg + if len(model.Regions) > 0 { + cfg.Regions = model.Regions + } + if model.Bucket != nil { + bucket := &cdn.BucketBackendPatch{ + Type: "bucket", + } + cfg.Backend = &cdn.ConfigPatchBackend{ + BucketBackendPatch: bucket, + } + if model.Bucket.URL != "" { + bucket.BucketUrl = utils.Ptr(model.Bucket.URL) + } + if model.Bucket.AccessKeyID != "" { + bucket.Credentials = cdn.NewBucketCredentials( + model.Bucket.AccessKeyID, + model.Bucket.Password, + ) + } + if model.Bucket.Region != "" { + bucket.Region = utils.Ptr(model.Bucket.Region) + } + } else if model.HTTP != nil { + http := &cdn.HttpBackendPatch{ + Type: "http", + } + cfg.Backend = &cdn.ConfigPatchBackend{ + HttpBackendPatch: http, + } + if model.HTTP.OriginRequestHeaders != nil { + http.OriginRequestHeaders = model.HTTP.OriginRequestHeaders + } + if model.HTTP.Geofencing != nil { + http.Geofencing = model.HTTP.Geofencing + } + if model.HTTP.OriginURL != "" { + http.OriginUrl = utils.Ptr(model.HTTP.OriginURL) + } + } + if len(model.BlockedCountries) > 0 { + cfg.BlockedCountries = model.BlockedCountries + } + if len(model.BlockedIPs) > 0 { + cfg.BlockedIps = model.BlockedIPs + } + if model.DefaultCacheDuration != "" { + cfg.DefaultCacheDuration = *cdn.NewNullableString(&model.DefaultCacheDuration) + } + if model.MonthlyLimitBytes != nil && *model.MonthlyLimitBytes > 0 { + cfg.MonthlyLimitBytes = *cdn.NewNullableInt64(model.MonthlyLimitBytes) + } + if model.Loki != nil { + loki := &cdn.LokiLogSinkPatch{} + cfg.LogSink = *cdn.NewNullableLokiLogSinkPatch(loki) + if model.Loki.PushURL != "" { + loki.PushUrl = utils.Ptr(model.Loki.PushURL) + } + if model.Loki.Username != "" { + loki.Credentials = cdn.NewLokiLogSinkCredentials( + model.Loki.Password, + model.Loki.Username, + ) + } + } + if model.Optimizer != nil { + cfg.Optimizer = &cdn.OptimizerPatch{ + Enabled: model.Optimizer, + } + } + req = req.PatchDistributionPayload(*payload) + return req +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *cdn.PatchDistributionResponse) error { + if resp == nil { + return fmt.Errorf("update distribution response is empty") + } + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Updated CDN distribution for %q. ID: %s\n", projectLabel, resp.Distribution.Id) + return nil + }) +} diff --git a/internal/cmd/beta/cdn/distribution/update/update_test.go b/internal/cmd/beta/cdn/distribution/update/update_test.go new file mode 100644 index 000000000..829d6522f --- /dev/null +++ b/internal/cmd/beta/cdn/distribution/update/update_test.go @@ -0,0 +1,368 @@ +package update + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const testCacheDuration = "P1DT12H" + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &cdn.APIClient{DefaultAPI: &cdn.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testDistributionID = uuid.NewString() + +const testMonthlyLimitBytes int64 = 1048576 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + DistributionID: testDistributionID, + Regions: nil, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(payload *cdn.PatchDistributionPayload)) cdn.ApiPatchDistributionRequest { + req := testClient.DefaultAPI.PatchDistribution(testCtx, testProjectId, testDistributionID) + if payload := fixturePayload(mods...); payload != nil { + req = req.PatchDistributionPayload(*fixturePayload(mods...)) + } + return req +} + +func fixturePayload(mods ...func(payload *cdn.PatchDistributionPayload)) *cdn.PatchDistributionPayload { + payload := cdn.NewPatchDistributionPayload() + payload.Config = &cdn.ConfigPatch{} + for _, m := range mods { + m(payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expected *inputModel + }{ + { + description: "base", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues(), + isValid: true, + expected: fixtureInputModel(), + }, + { + description: "distribution id missing", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid distribution id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid distribution id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "both backends", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagHTTP] = "true" + flagValues[flagBucket] = "true" + }, + ), + isValid: false, + }, + { + description: "max config without backend", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagRegions.Name()] = "EU,US" + flagValues[flagBlockedCountries] = "DE,AT,CH" + flagValues[flagBlockedIPs] = "127.0.0.1,10.0.0.8" + flagValues[flagDefaultCacheDuration] = "P1DT12H" + flagValues[flagLoki] = "true" + flagValues[flagLokiUsername] = "loki-user" + flagValues[flagLokiPushURL] = "https://loki.example.com" + flagValues[flagMonthlyLimitBytes] = fmt.Sprintf("%d", testMonthlyLimitBytes) + flagValues[flagOptimizer] = "true" + }, + ), + isValid: true, + expected: fixtureInputModel( + func(model *inputModel) { + model.Regions = []cdn.Region{cdn.REGION_EU, cdn.REGION_US} + model.BlockedCountries = []string{"DE", "AT", "CH"} + model.BlockedIPs = []string{"127.0.0.1", "10.0.0.8"} + model.DefaultCacheDuration = "P1DT12H" + model.Loki = &lokiInputModel{ + Username: "loki-user", + PushURL: "https://loki.example.com", + } + model.MonthlyLimitBytes = utils.Ptr(testMonthlyLimitBytes) + model.Optimizer = utils.Ptr(true) + }, + ), + }, + { + description: "max config http backend", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagHTTP] = "true" + flagValues[flagHTTPOriginURL] = "https://origin.example.com" + flagValues[flagHTTPOriginRequestHeaders] = "X-Example-Header: example-value, X-Another-Header: another-value" + flagValues[flagHTTPGeofencing] = "https://dach.example.com DE,AT,CH" + }, + ), + isValid: true, + expected: fixtureInputModel( + func(model *inputModel) { + model.HTTP = &httpInputModel{ + OriginURL: "https://origin.example.com", + OriginRequestHeaders: &map[string]string{ + "X-Example-Header": "example-value", + "X-Another-Header": "another-value", + }, + Geofencing: &map[string][]string{ + "https://dach.example.com": {"DE", "AT", "CH"}, + }, + } + }, + ), + }, + { + description: "max config bucket backend", + argValues: []string{testDistributionID}, + flagValues: fixtureFlagValues( + func(flagValues map[string]string) { + flagValues[flagBucket] = "true" + flagValues[flagBucketURL] = "https://bucket.example.com" + flagValues[flagBucketRegion] = "EU" + flagValues[flagBucketCredentialsAccessKeyID] = "access-key-id" + }, + ), + isValid: true, + expected: fixtureInputModel( + func(model *inputModel) { + model.Bucket = &bucketInputModel{ + URL: "https://bucket.example.com", + Region: "EU", + AccessKeyID: "access-key-id", + } + }, + ), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expected, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected cdn.ApiPatchDistributionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expected: fixtureRequest(), + }, + { + description: "max without backend", + model: fixtureInputModel( + func(model *inputModel) { + model.Regions = []cdn.Region{cdn.REGION_EU, cdn.REGION_US} + model.BlockedCountries = []string{"DE", "AT", "CH"} + model.BlockedIPs = []string{"127.0.0.1", "10.0.0.8"} + model.DefaultCacheDuration = testCacheDuration + model.MonthlyLimitBytes = utils.Ptr(testMonthlyLimitBytes) + model.Loki = &lokiInputModel{ + Password: "loki-pass", + Username: "loki-user", + PushURL: "https://loki.example.com", + } + model.Optimizer = utils.Ptr(true) + }, + ), + expected: fixtureRequest( + func(payload *cdn.PatchDistributionPayload) { + payload.Config.Regions = []cdn.Region{cdn.REGION_EU, cdn.REGION_US} + payload.Config.BlockedCountries = []string{"DE", "AT", "CH"} + payload.Config.BlockedIps = []string{"127.0.0.1", "10.0.0.8"} + payload.Config.DefaultCacheDuration = *cdn.NewNullableString(utils.Ptr(testCacheDuration)) + monthlyLimitBytes := testMonthlyLimitBytes + payload.Config.MonthlyLimitBytes = *cdn.NewNullableInt64(&monthlyLimitBytes) + payload.Config.LogSink = *cdn.NewNullableLokiLogSinkPatch( + &cdn.LokiLogSinkPatch{ + Credentials: cdn.NewLokiLogSinkCredentials("loki-pass", "loki-user"), + PushUrl: utils.Ptr("https://loki.example.com"), + }, + ) + payload.Config.Optimizer = &cdn.OptimizerPatch{ + Enabled: utils.Ptr(true), + } + }, + ), + }, + { + description: "max http backend", + model: fixtureInputModel( + func(model *inputModel) { + model.HTTP = &httpInputModel{ + Geofencing: &map[string][]string{"https://dach.example.com": {"DE", "AT", "CH"}}, + OriginRequestHeaders: &map[string]string{"X-Example-Header": "example-value", "X-Another-Header": "another-value"}, + OriginURL: "https://http-backend.example.com", + } + }), + expected: fixtureRequest( + func(payload *cdn.PatchDistributionPayload) { + payload.Config.Backend = &cdn.ConfigPatchBackend{ + HttpBackendPatch: &cdn.HttpBackendPatch{ + Geofencing: &map[string][]string{"https://dach.example.com": {"DE", "AT", "CH"}}, + OriginRequestHeaders: &map[string]string{ + "X-Example-Header": "example-value", + "X-Another-Header": "another-value", + }, + OriginUrl: utils.Ptr("https://http-backend.example.com"), + Type: "http", + }, + } + }), + }, + { + description: "max bucket backend", + model: fixtureInputModel( + func(model *inputModel) { + model.Bucket = &bucketInputModel{ + URL: "https://bucket.example.com", + AccessKeyID: "bucket-access-key-id", + Password: "bucket-pass", + Region: "EU", + } + }), + expected: fixtureRequest( + func(payload *cdn.PatchDistributionPayload) { + payload.Config.Backend = &cdn.ConfigPatchBackend{ + BucketBackendPatch: &cdn.BucketBackendPatch{ + BucketUrl: utils.Ptr("https://bucket.example.com"), + Credentials: cdn.NewBucketCredentials("bucket-access-key-id", "bucket-pass"), + Region: utils.Ptr("EU"), + Type: "bucket", + }, + } + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.model) + + diff := cmp.Diff(request, tt.expected, + cmp.AllowUnexported( + cdn.ApiPatchDistributionRequest{}, + cdn.NullableString{}, + cdn.NullableInt64{}, + cdn.NullableLokiLogSinkPatch{}, + cdn.DefaultAPIService{}, + ), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFormat string + response *cdn.PatchDistributionResponse + expected string + wantErr bool + }{ + { + description: "nil response", + outputFormat: "table", + response: nil, + wantErr: true, + }, + { + description: "table output", + outputFormat: "table", + response: &cdn.PatchDistributionResponse{ + Distribution: cdn.Distribution{ + Id: "dist-1234", + }, + }, + expected: fmt.Sprintf("Updated CDN distribution for %q. ID: dist-1234\n", testProjectId), + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, testProjectId, tt.response); (err != nil) != tt.wantErr { + t.Fatalf("outputResult: %v", err) + } + if params.Out.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, params.Out.String()) + } + }) + } +} diff --git a/internal/cmd/beta/edge/edge.go b/internal/cmd/beta/edge/edge.go new file mode 100644 index 000000000..35b5e0575 --- /dev/null +++ b/internal/cmd/beta/edge/edge.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package edge + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/kubeconfig" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/plans" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/token" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "edge-cloud", + Short: "Provides functionality for edge services.", + Long: "Provides functionality for STACKIT Edge Cloud (STEC) services.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(instance.NewCmd(params)) + cmd.AddCommand(plans.NewCmd(params)) + cmd.AddCommand(kubeconfig.NewCmd(params)) + cmd.AddCommand(token.NewCmd(params)) +} diff --git a/internal/cmd/beta/edge/instance/create/create.go b/internal/cmd/beta/edge/instance/create/create.go new file mode 100755 index 000000000..6faac9cc0 --- /dev/null +++ b/internal/cmd/beta/edge/instance/create/create.go @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// Command constructor +// Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags +// instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname +// is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we +// won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates an edge instance", + Long: "Creates a STACKIT Edge Cloud (STEC) instance. The instance will take a moment to become fully functional.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + fmt.Sprintf(`Creates an edge instance with the %s "xxx" and %s "yyy"`, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance create --%s "xxx" --%s "yyy"`, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + // If project label can't be determined, fall back to project ID + projectLabel = model.ProjectId + } + + // Prompt for confirmation + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to create a new edge instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + resp, err := run(ctx, model, apiClient) + if err != nil { + return err + } + if resp == nil { + return fmt.Errorf("create instance: empty response from API") + } + if resp.Id == nil { + return fmt.Errorf("create instance: instance id missing in response") + } + instanceId := *resp.Id + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating instance", func() error { + // The waiter handler needs a concrete concreteClient type. We can safely cast here as the real implementation will always match. + concreteClient, ok := apiClient.(*edge.APIClient) + if !ok { + return fmt.Errorf("failed to configure API concreteClient") + } + _, err = wait.CreateOrUpdateInstanceWaitHandler(ctx, concreteClient, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for edge instance creation: %w", err) + } + } + + // Handle output to printer + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +// inputModel represents the user input for creating an edge instance. +type inputModel struct { + *globalflags.GlobalFlagModel + DisplayName string + Description string + PlanId string +} + +// createRequestSpec captures the details of the request for testing. +type createRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + Payload edge.CreateInstancePayload + + // Execute is a closure that wraps the actual SDK call + Execute func() (*edge.Instance, error) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) + cmd.Flags().StringP(commonInstance.DescriptionFlag, commonInstance.DescriptionShorthand, "", commonInstance.DescriptionUsage) + cmd.Flags().String(commonInstance.PlanIdFlag, "", commonInstance.PlanIdUsage) + + cobra.CheckErr(flags.MarkFlagsRequired(cmd, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag)) +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Parse and validate user input then add it to the model + displayNameValue := flags.FlagToStringPointer(p, cmd, commonInstance.DisplayNameFlag) + if err := commonInstance.ValidateDisplayName(displayNameValue); err != nil { + return nil, err + } + + planIdValue := flags.FlagToStringPointer(p, cmd, commonInstance.PlanIdFlag) + if err := commonInstance.ValidatePlanId(planIdValue); err != nil { + return nil, err + } + + descriptionValue := flags.FlagWithDefaultToStringValue(p, cmd, commonInstance.DescriptionFlag) + if err := commonInstance.ValidateDescription(descriptionValue); err != nil { + return nil, err + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DisplayName: *displayNameValue, + Description: descriptionValue, + PlanId: *planIdValue, + } + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Instance, error) { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return nil, err + } + + resp, err := spec.Execute() + if err != nil { + return nil, cliErr.NewRequestFailedError(err) + } + + return resp, nil +} + +// buildRequest constructs the spec that can be tested. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*createRequestSpec, error) { + req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) + + // Build request payload + payload := edge.CreateInstancePayload{ + DisplayName: &model.DisplayName, + Description: &model.Description, + PlanId: &model.PlanId, + } + req = req.CreateInstancePayload(payload) + + return &createRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + Payload: payload, + Execute: req.Execute, + }, nil +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, instance *edge.Instance) error { + if instance == nil { + // This is only to prevent nil pointer deref. + // As long as the API behaves as defined by it's spec, instance can not be empty (HTTP 200 with an empty body) + return commonErr.NewNoInstanceError("") + } + + return p.OutputResult(outputFormat, instance, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + p.Outputf("%s instance for project %q. Instance ID: %q.\n", operationState, projectLabel, utils.PtrString(instance.Id)) + return nil + }) +} diff --git a/internal/cmd/beta/edge/instance/create/create_test.go b/internal/cmd/beta/edge/instance/create/create_test.go new file mode 100755 index 000000000..13e64a6ca --- /dev/null +++ b/internal/cmd/beta/edge/instance/create/create_test.go @@ -0,0 +1,419 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package create + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" + + testName = "test" + testPlanId = uuid.NewString() + testDescription = "Initial instance description" + testInstanceId = uuid.NewString() +) + +// mockExecutable is a mock for the Executable interface used by the SDK +type mockExecutable struct { + executeFails bool + resp *edge.Instance +} + +func (m *mockExecutable) CreateInstancePayload(_ edge.CreateInstancePayload) edge.ApiCreateInstanceRequest { + // This method is needed to satisfy the interface. It allows chaining in buildRequest. + return m +} +func (m *mockExecutable) Execute() (*edge.Instance, error) { + if m.executeFails { + return nil, errors.New("API error") + } + if m.resp != nil { + return m.resp, nil + } + return &edge.Instance{Id: &testInstanceId}, nil +} + +// mockAPIClient is a mock for the client.APIClient interface +type mockAPIClient struct { + createInstanceMock edge.ApiCreateInstanceRequest +} + +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + if m.createInstanceMock != nil { + return m.createInstanceMock + } + return &mockExecutable{} +} + +// Unused methods to satisfy the client.APIClient interface +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + commonInstance.DisplayNameFlag: testName, + commonInstance.DescriptionFlag: testDescription, + commonInstance.PlanIdFlag: testPlanId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: testName, + Description: testDescription, + PlanId: testPlanId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "create success", + want: fixtureInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}, globalflags.GlobalFlagModel{}), + }, + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "name missing", + wantErr: "required flag(s) \"name\" not set", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.DisplayNameFlag) + }), + }, + }, + { + name: "name too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = "this-name-is-way-too-long-for-the-validation" + }), + }, + }, + { + name: "name too short", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = "in" + }), + }, + }, + { + name: "name invalid", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = "1test" + }), + }, + }, + { + name: "plan invalid", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.PlanIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "description too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DescriptionFlag] = strings.Repeat("a", 257) + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + tests := []struct { + name string + args args + want *createRequestSpec + }{ + { + name: "success", + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + createInstanceMock: &mockExecutable{}, + }, + }, + want: &createRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + Payload: edge.CreateInstancePayload{ + DisplayName: &testName, + Description: &testDescription, + PlanId: &testPlanId, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := buildRequest(testCtx, tt.args.model, tt.args.client) + + if got != nil { + if got.Execute == nil { + t.Error("expected non-nil Execute function") + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(createRequestSpec{}, "Execute")) + } + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *edge.Instance + args args + }{ + { + name: "create success", + want: &edge.Instance{Id: &testInstanceId}, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + createInstanceMock: &mockExecutable{ + resp: &edge.Instance{Id: &testInstanceId}, + }, + }, + }, + }, + { + name: "create API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + createInstanceMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := run(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + instance *edge.Instance + projectLabel string + } + + tests := []struct { + name string + wantErr error + args args + }{ + { + name: "no instance", + wantErr: &commonErr.NoInstanceError{}, + args: args{ + model: fixtureInputModel(), + }, + }, + { + name: "output json", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + instance: &edge.Instance{}, + }, + }, + { + name: "output yaml", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + }), + instance: &edge.Instance{}, + }, + }, + { + name: "output default", + args: args{ + model: fixtureInputModel(), + instance: &edge.Instance{Id: &testInstanceId}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.args.model.OutputFormat, tt.args.model.Async, tt.args.projectLabel, tt.args.instance) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} diff --git a/internal/cmd/beta/edge/instance/delete/delete.go b/internal/cmd/beta/edge/instance/delete/delete.go new file mode 100755 index 000000000..c6998e782 --- /dev/null +++ b/internal/cmd/beta/edge/instance/delete/delete.go @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +// Struct to model user input (arguments and/or flags) +type inputModel struct { + *globalflags.GlobalFlagModel + identifier *commonValidation.Identifier +} + +// deleteRequestSpec captures the details of a request for testing. +type deleteRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + InstanceId string // Set if deleting by ID + InstanceName string // Set if deleting by Name + + // Execute is a closure that wraps the actual SDK call + Execute func() error +} + +// OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. +// InstanceWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. +type instanceWaiter interface { + WaitWithContext(context.Context) (*edge.Instance, error) +} + +// A function that creates an instance waiter +type instanceWaiterFactory = func(client *edge.APIClient) instanceWaiter + +// Command constructor +// Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags +// instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname +// is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we +// won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes an edge instance", + Long: "Deletes a STACKIT Edge Cloud (STEC) instance. The instance will be deleted permanently.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + fmt.Sprintf(`Delete an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance delete --%s "xxx"`, commonInstance.InstanceIdFlag)), + examples.NewExample( + fmt.Sprintf(`Delete an edge instance with %s "xxx"`, commonInstance.DisplayNameFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance delete --%s "xxx"`, commonInstance.DisplayNameFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + // If project label can't be determined, fall back to project ID + projectLabel = model.ProjectId + } + + // Prompt for confirmation + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete the edge instance %q of project %q?", model.identifier.Value, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + err = run(ctx, model, apiClient) + if err != nil { + return err + } + + // Wait for async operation, if async mode not enabled + operationState := "Triggered deletion of" + if !model.Async { + err := spinner.Run(params.Printer, "Deleting instance", func() error { + // Determine identifier and waiter to use + waiterFactory, err := getWaiterFactory(ctx, model) + if err != nil { + return err + } + // The waiter factory needs a concrete concreteClient type. We can safely cast here as the real implementation will always match. + concreteClient, ok := apiClient.(*edge.APIClient) + if !ok { + return fmt.Errorf("failed to configure API concreteClient") + } + waiter := waiterFactory(concreteClient) + _, err = waiter.WaitWithContext(ctx) + return err + }) + + if err != nil { + return fmt.Errorf("wait for edge instance deletion: %w", err) + } + operationState = "Deleted" + } + + params.Printer.Info("%s instance with %q %q of project %q.\n", operationState, model.identifier.Flag, model.identifier.Value, projectLabel) + + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) + cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) + + identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} + cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName + cmd.MarkFlagsOneRequired(identifierFlags...) +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Generate input model based on chosen flags + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + // Parse and validate user input then add it to the model + id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) + if err != nil { + return nil, err + } + model.identifier = id + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) error { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + + if err := spec.Execute(); err != nil { + return cliErr.NewRequestFailedError(err) + } + + return nil +} + +// buildRequest constructs the spec that can be tested. +// It handles the logic of choosing between DeleteInstance and DeleteInstanceByName. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*deleteRequestSpec, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + spec := &deleteRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + } + + // Switch the concrete client based on the identifier flag used + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + spec.InstanceId = model.identifier.Value + req := apiClient.DeleteInstance(ctx, model.ProjectId, model.Region, model.identifier.Value) + spec.Execute = req.Execute + case commonInstance.DisplayNameFlag: + spec.InstanceName = model.identifier.Value + req := apiClient.DeleteInstanceByName(ctx, model.ProjectId, model.Region, model.identifier.Value) + spec.Execute = req.Execute + default: + return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) + } + + return spec, nil +} + +// Returns a factory function to create the appropriate waiter based on the input model. +func getWaiterFactory(ctx context.Context, model *inputModel) (instanceWaiterFactory, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + factory := func(c *edge.APIClient) instanceWaiter { + return wait.DeleteInstanceWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) + } + return factory, nil + case commonInstance.DisplayNameFlag: + factory := func(c *edge.APIClient) instanceWaiter { + return wait.DeleteInstanceByNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) + } + return factory, nil + default: + return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) + } +} diff --git a/internal/cmd/beta/edge/instance/delete/delete_test.go b/internal/cmd/beta/edge/instance/delete/delete_test.go new file mode 100755 index 000000000..3e72a71d1 --- /dev/null +++ b/internal/cmd/beta/edge/instance/delete/delete_test.go @@ -0,0 +1,558 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package delete + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" + + testInstanceId = "instance" + testDisplayName = "test" +) + +// mockExecutable implements the SDK delete request interface for testing. +type mockExecutable struct { + executeFails bool + executeNotFound bool +} + +func (m *mockExecutable) Execute() error { + if m.executeNotFound { + return &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusNotFound, + Body: []byte(`{"message":"not found"}`), + } + } + if m.executeFails { + return errors.New("execute failed") + } + return nil +} + +// mockAPIClient provides the minimal API client behavior required by the tests. +type mockAPIClient struct { + deleteInstanceMock edge.ApiDeleteInstanceRequest + deleteInstanceByNameMock edge.ApiDeleteInstanceByNameRequest +} + +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + if m.deleteInstanceMock != nil { + return m.deleteInstanceMock + } + return &mockExecutable{} +} + +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + if m.deleteInstanceByNameMock != nil { + return m.deleteInstanceByNameMock + } + return &mockExecutable{} +} + +// Unused methods to satisfy the client.APIClient interface. +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + commonInstance.InstanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(useDisplayName bool, mods ...func(*inputModel)) *inputModel { + identifier := &commonValidation.Identifier{ + Flag: commonInstance.InstanceIdFlag, + Value: testInstanceId, + } + if useDisplayName { + identifier = &commonValidation.Identifier{ + Flag: commonInstance.DisplayNameFlag, + Value: testDisplayName, + } + } + + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + identifier: identifier, + } + + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureByIdInputModel(mods ...func(*inputModel)) *inputModel { + return fixtureInputModel(false, mods...) +} + +func fixtureByNameInputModel(mods ...func(*inputModel)) *inputModel { + return fixtureInputModel(true, mods...) +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "by id", + want: fixtureByIdInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}, globalflags.GlobalFlagModel{}), + }, + }, + }, + { + name: "by name", + want: fixtureByNameInputModel(), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}, globalflags.GlobalFlagModel{}), + }, + }, + }, + { + name: "by id and name", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "instance id empty", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "" + }), + }, + }, + { + name: "instance id too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" + }), + }, + }, + { + name: "instance id too short", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "id" + }), + }, + }, + { + name: "name too short", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foo" + }), + }, + }, + { + name: "name too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + tests := []struct { + name string + args args + wantErr error + }{ + { + name: "delete by id success", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + deleteInstanceMock: &mockExecutable{}, + }, + }, + }, + { + name: "delete by id API error", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + deleteInstanceMock: &mockExecutable{executeFails: true}, + }, + }, + wantErr: &cliErr.RequestFailedError{}, + }, + { + name: "delete by id not found", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + deleteInstanceMock: &mockExecutable{executeNotFound: true}, + }, + }, + wantErr: &cliErr.RequestFailedError{}, + }, + { + name: "delete by name success", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + deleteInstanceByNameMock: &mockExecutable{}, + }, + }, + }, + { + name: "delete by name API error", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + deleteInstanceByNameMock: &mockExecutable{executeFails: true}, + }, + }, + wantErr: &cliErr.RequestFailedError{}, + }, + { + name: "delete by name not found", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + deleteInstanceByNameMock: &mockExecutable{executeNotFound: true}, + }, + }, + wantErr: &cliErr.RequestFailedError{}, + }, + { + name: "no identifier", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + wantErr: &commonErr.NoIdentifierError{}, + }, + { + name: "invalid identifier", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = &commonValidation.Identifier{Flag: "unknown", Value: "value"} + }), + client: &mockAPIClient{}, + }, + wantErr: &cliErr.BuildRequestError{}, + }, + { + name: "nil model", + args: args{ + model: nil, + client: &mockAPIClient{}, + }, + wantErr: &commonErr.NoIdentifierError{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := run(testCtx, tt.args.model, tt.args.client) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + tests := []struct { + name string + args args + want *deleteRequestSpec + wantErr error + }{ + { + name: "by id", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + deleteInstanceMock: &mockExecutable{}, + }, + }, + want: &deleteRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceId: testInstanceId, + }, + }, + { + name: "by name", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + deleteInstanceByNameMock: &mockExecutable{}, + }, + }, + want: &deleteRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceName: testDisplayName, + }, + }, + { + name: "no identifier", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + wantErr: &commonErr.NoIdentifierError{}, + }, + { + name: "invalid identifier", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = &commonValidation.Identifier{Flag: "unknown", Value: "val"} + }), + client: &mockAPIClient{}, + }, + wantErr: &cliErr.BuildRequestError{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + if got != nil { + if got.Execute == nil { + t.Error("expected non-nil Execute function") + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(deleteRequestSpec{}, "Execute")) + } + }) + } +} + +func TestGetWaiterFactory(t *testing.T) { + type args struct { + model *inputModel + } + + tests := []struct { + name string + wantErr error + want bool + args args + }{ + { + name: "by id identifier", + want: true, + args: args{ + model: fixtureByIdInputModel(), + }, + }, + { + name: "by name identifier", + want: true, + args: args{ + model: fixtureByNameInputModel(), + }, + }, + { + name: "nil model", + wantErr: &commonErr.NoIdentifierError{}, + want: false, + args: args{ + model: nil, + }, + }, + { + name: "nil identifier", + wantErr: &commonErr.NoIdentifierError{}, + want: false, + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = nil + }), + }, + }, + { + name: "invalid identifier", + wantErr: &commonErr.InvalidIdentifierError{}, + want: false, + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = &commonValidation.Identifier{Flag: "unsupported", Value: "value"} + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getWaiterFactory(testCtx, tt.args.model) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + if tt.want && got == nil { + t.Fatal("expected non-nil waiter factory") + } + if !tt.want && got != nil { + t.Fatal("expected nil waiter factory") + } + }) + } +} diff --git a/internal/cmd/beta/edge/instance/describe/describe.go b/internal/cmd/beta/edge/instance/describe/describe.go new file mode 100755 index 000000000..d4ac397b5 --- /dev/null +++ b/internal/cmd/beta/edge/instance/describe/describe.go @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + identifier *commonValidation.Identifier +} + +// describeRequestSpec captures the details of the request for testing. +type describeRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + InstanceId string // Set if describing by ID + InstanceName string // Set if describing by Name + + // Execute is a closure that wraps the actual SDK call + Execute func() (*edge.Instance, error) +} + +// Command constructor +// Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags +// instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname +// is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we +// won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describes an edge instance", + Long: "Describes a STACKIT Edge Cloud (STEC) instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + fmt.Sprintf(`Describe an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance describe --%s `, commonInstance.InstanceIdFlag)), + examples.NewExample( + fmt.Sprintf(`Describe an edge instance with %s "xxx"`, commonInstance.DisplayNameFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance describe --%s `, commonInstance.DisplayNameFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + resp, err := run(ctx, model, apiClient) + if err != nil { + return err + } + + // Handle output to printer + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) + cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) + + identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} + cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName + cmd.MarkFlagsOneRequired(identifierFlags...) +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Generate input model based on chosen flags + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + // Parse and validate user input then add it to the model + id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) + if err != nil { + return nil, err + } + model.identifier = id + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Instance, error) { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return nil, err + } + + resp, err := spec.Execute() + if err != nil { + return nil, cliErr.NewRequestFailedError(err) + } + + return resp, nil +} + +// buildRequest constructs the spec that can be tested. +// It handles the logic of choosing between GetInstance and GetInstanceByName. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*describeRequestSpec, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + spec := &describeRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + } + + // Switch the concrete client based on the identifier flag used + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + spec.InstanceId = model.identifier.Value + req := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.identifier.Value) + spec.Execute = req.Execute + case commonInstance.DisplayNameFlag: + spec.InstanceName = model.identifier.Value + req := apiClient.GetInstanceByName(ctx, model.ProjectId, model.Region, model.identifier.Value) + spec.Execute = req.Execute + default: + return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) + } + + return spec, nil +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat string, instance *edge.Instance) error { + if instance == nil { + // This is only to prevent nil pointer deref. + // As long as the API behaves as defined by it's spec, instance can not be empty (HTTP 200 with an empty body) + return commonErr.NewNoInstanceError("") + } + + return p.OutputResult(outputFormat, instance, func() error { + table := tables.NewTable() + // Describe: output all fields. Be sure to filter for any non-required fields. + table.AddRow("CREATED", utils.PtrString(instance.Created)) + table.AddSeparator() + table.AddRow("ID", utils.PtrString(instance.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(instance.DisplayName)) + table.AddSeparator() + if instance.HasDescription() { + table.AddRow("DESCRIPTION", utils.PtrString(instance.Description)) + table.AddSeparator() + } + table.AddRow("UI", utils.PtrString(instance.FrontendUrl)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(instance.Status)) + table.AddSeparator() + table.AddRow("PLAN", utils.PtrString(instance.PlanId)) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/edge/instance/describe/describe_test.go b/internal/cmd/beta/edge/instance/describe/describe_test.go new file mode 100755 index 000000000..4ad849200 --- /dev/null +++ b/internal/cmd/beta/edge/instance/describe/describe_test.go @@ -0,0 +1,575 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package describe + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" + + testInstanceId = "instance" + testDisplayName = "test" +) + +// mockExecutable is a mock for the Executable interface +type mockExecutable struct { + executeFails bool + executeNotFound bool + executeResp *edge.Instance +} + +func (m *mockExecutable) Execute() (*edge.Instance, error) { + if m.executeFails { + return nil, errors.New("API error") + } + if m.executeNotFound { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusNotFound, + } + } + return m.executeResp, nil +} + +// mockAPIClient is a mock for the edge.APIClient interface +type mockAPIClient struct { + getInstanceMock edge.ApiGetInstanceRequest + getInstanceByNameMock edge.ApiGetInstanceByNameRequest +} + +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + if m.getInstanceMock != nil { + return m.getInstanceMock + } + return &mockExecutable{} +} + +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + if m.getInstanceByNameMock != nil { + return m.getInstanceByNameMock + } + return &mockExecutable{} +} + +// Unused methods to satisfy the interface +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + commonInstance.InstanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(false, mods...) +} + +func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(true, mods...) +} + +func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + + if useName { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.DisplayNameFlag, + Value: testDisplayName, + } + } else { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.InstanceIdFlag, + Value: testInstanceId, + } + } + + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "by id", + want: fixtureByIdInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by name", + want: fixtureByNameInputModel(), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by id and name", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "instanceId missing", + want: fixtureByNameInputModel(), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "instanceId empty", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "" + }), + }, + }, + { + name: "instanceId too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" + }), + }, + }, + { + name: "instanceId too short", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "id" + }), + }, + }, + { + name: "name too short", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foo" + }), + }, + }, + { + name: "name too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *edge.Instance + args args + }{ + { + name: "get by id success", + want: &edge.Instance{ + Id: &testInstanceId, + DisplayName: &testDisplayName, + }, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + getInstanceMock: &mockExecutable{ + executeResp: &edge.Instance{ + Id: &testInstanceId, + DisplayName: &testDisplayName, + }, + }, + }, + }, + }, + { + name: "get by name success", + want: &edge.Instance{ + Id: &testInstanceId, + DisplayName: &testDisplayName, + }, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + getInstanceByNameMock: &mockExecutable{ + executeResp: &edge.Instance{ + Id: &testInstanceId, + DisplayName: &testDisplayName, + }, + }, + }, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + }, + { + name: "instance not found error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + getInstanceMock: &mockExecutable{ + executeNotFound: true, + }, + }, + }, + }, + { + name: "get by id API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + getInstanceMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + { + name: "get by name API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + getInstanceByNameMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := run(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestOutputResult(t *testing.T) { + type outputArgs struct { + model *inputModel + instance *edge.Instance + } + + tests := []struct { + name string + wantErr error + args outputArgs + }{ + { + name: "no instance", + wantErr: &commonErr.NoInstanceError{}, + args: outputArgs{ + model: fixtureByIdInputModel(), + instance: nil, + }, + }, + { + name: "output json", + args: outputArgs{ + model: fixtureInputModel(false, func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + model.identifier = nil + }), + instance: &edge.Instance{}, + }, + }, + { + name: "output yaml", + args: outputArgs{ + model: fixtureInputModel(false, func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + model.identifier = nil + }), + instance: &edge.Instance{}, + }, + }, + { + name: "output default", + args: outputArgs{ + model: fixtureByIdInputModel(), + instance: &edge.Instance{Id: &testInstanceId}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.args.model.OutputFormat, tt.args.instance) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *describeRequestSpec + args args + }{ + { + name: "get by id", + want: &describeRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceId: testInstanceId, + }, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + getInstanceMock: &mockExecutable{}, + }, + }, + }, + { + name: "get by name", + want: &describeRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceName: testDisplayName, + }, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + getInstanceByNameMock: &mockExecutable{}, + }, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(describeRequestSpec{}, "Execute")) + }) + } +} diff --git a/internal/cmd/beta/edge/instance/instance.go b/internal/cmd/beta/edge/instance/instance.go new file mode 100644 index 000000000..748371cda --- /dev/null +++ b/internal/cmd/beta/edge/instance/instance.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package instance + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/instance/update" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "instance", + Short: "Provides functionality for edge instances.", + Long: "Provides functionality for STACKIT Edge Cloud (STEC) instance management.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) +} diff --git a/internal/cmd/beta/edge/instance/list/list.go b/internal/cmd/beta/edge/instance/list/list.go new file mode 100755 index 000000000..3b728ee2d --- /dev/null +++ b/internal/cmd/beta/edge/instance/list/list.go @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + limitFlag = "limit" +) + +// Struct to model user input (arguments and/or flags) +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +// listRequestSpec captures the details of the request for testing. +type listRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + Limit *int64 + + // Execute is a closure that wraps the actual SDK call + Execute func() (*edge.InstanceList, error) +} + +// Command constructor +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists edge instances", + Long: "Lists STACKIT Edge Cloud (STEC) instances of a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists all edge instances of a given project`, + `$ stackit beta edge-cloud instance list`), + examples.NewExample( + `Lists all edge instances of a given project and limits the output to two instances`, + fmt.Sprintf(`$ stackit beta edge-cloud instance list --%s 2`, limitFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + // If project label can't be determined, fall back to project ID + projectLabel = model.ProjectId + } + + // Call API + resp, err := run(ctx, model, apiClient) + if err != nil { + return err + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Parse and validate user input then add it to the model + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) ([]edge.Instance, error) { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return nil, err + } + + resp, err := spec.Execute() + if err != nil { + return nil, cliErr.NewRequestFailedError(err) + } + if resp == nil { + return nil, fmt.Errorf("list instances: empty response from API") + } + if resp.Instances == nil { + return nil, fmt.Errorf("list instances: instances missing in response") + } + instances := *resp.Instances + + // Truncate output if limit is set + if spec.Limit != nil && len(instances) > int(*spec.Limit) { + instances = instances[:*spec.Limit] + } + + return instances, nil +} + +// buildRequest constructs the spec that can be tested. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*listRequestSpec, error) { + req := apiClient.ListInstances(ctx, model.ProjectId, model.Region) + + return &listRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + Limit: model.Limit, + Execute: req.Execute, + }, nil +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []edge.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + // No instances found for project + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil + } + + // Display instances found for project in a table + table := tables.NewTable() + // List: only output the most important fields. Be sure to filter for any non-required fields. + table.SetHeader("ID", "NAME", "UI", "STATE") + for i := range instances { + instance := instances[i] + table.AddRow( + utils.PtrString(instance.Id), + utils.PtrString(instance.DisplayName), + utils.PtrString(instance.FrontendUrl), + utils.PtrString(instance.Status)) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/edge/instance/list/list_test.go b/internal/cmd/beta/edge/instance/list/list_test.go new file mode 100755 index 000000000..8a9188370 --- /dev/null +++ b/internal/cmd/beta/edge/instance/list/list_test.go @@ -0,0 +1,460 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package list + +import ( + "context" + "errors" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" +) + +// mockExecutable is a mock for the Executable interface +type mockExecutable struct { + executeFails bool + executeResp *edge.InstanceList +} + +func (m *mockExecutable) Execute() (*edge.InstanceList, error) { + if m.executeFails { + return nil, errors.New("API error") + } + + if m.executeResp != nil { + return m.executeResp, nil + } + return &edge.InstanceList{ + Instances: &[]edge.Instance{ + {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, + {Id: utils.Ptr("instance-2"), DisplayName: utils.Ptr("nameb")}, + }, + }, nil +} + +// mockAPIClient is a mock for the edge.APIClient interface +type mockAPIClient struct { + listInstancesMock edge.ApiListInstancesRequest +} + +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + if m.listInstancesMock != nil { + return m.listInstancesMock + } + return &mockExecutable{} +} + +// Unused methods to satisfy the interface +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "success", + want: fixtureInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "with limit", + want: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(10)) + }), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "10" + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "limit invalid", + wantErr: "invalid syntax", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + }, + }, + { + name: "limit less than 1", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want []edge.Instance + args args + }{ + { + name: "list success", + want: []edge.Instance{ + {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, + {Id: utils.Ptr("instance-2"), DisplayName: utils.Ptr("nameb")}, + }, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{}, + }, + }, + { + name: "list success with limit", + want: []edge.Instance{ + {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, + }, + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(1)) + }), + client: &mockAPIClient{}, + }, + }, + { + name: "list success with limit greater than items", + want: []edge.Instance{ + {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, + {Id: utils.Ptr("instance-2"), DisplayName: utils.Ptr("nameb")}, + }, + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(5)) + }), + client: &mockAPIClient{}, + }, + }, + { + name: "list success with no items", + want: []edge.Instance{}, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + listInstancesMock: &mockExecutable{ + executeResp: &edge.InstanceList{Instances: &[]edge.Instance{}}, + }, + }, + }, + }, + { + name: "list API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + listInstancesMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := run(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + instances []edge.Instance + projectLabel string + } + + tests := []struct { + name string + wantErr error + args args + }{ + { + name: "no instance", + args: args{ + model: fixtureInputModel(), + }, + }, + { + name: "output json", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + instances: []edge.Instance{ + {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, + }, + projectLabel: "test-project", + }, + }, + { + name: "output yaml", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + }), + instances: []edge.Instance{ + {Id: utils.Ptr("instance-1"), DisplayName: utils.Ptr("namea")}, + }, + projectLabel: "test-project", + }, + }, + { + name: "output default with instances", + args: args{ + model: fixtureInputModel(), + instances: []edge.Instance{ + { + Id: utils.Ptr("instance-1"), + DisplayName: utils.Ptr("namea"), + FrontendUrl: utils.Ptr("https://example.com"), + }, + { + Id: utils.Ptr("instance-2"), + DisplayName: utils.Ptr("nameb"), + FrontendUrl: utils.Ptr("https://example2.com"), + }, + }, + projectLabel: "test-project", + }, + }, + { + name: "output default with no instances", + args: args{ + model: fixtureInputModel(), + instances: []edge.Instance{}, + projectLabel: "test-project", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.args.model.OutputFormat, tt.args.projectLabel, tt.args.instances) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *listRequestSpec + args args + }{ + { + name: "success", + want: &listRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + }, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + listInstancesMock: &mockExecutable{}, + }, + }, + }, + { + name: "success with limit", + want: &listRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + Limit: utils.Ptr(int64(10)), + }, + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(10)) + }), + client: &mockAPIClient{ + listInstancesMock: &mockExecutable{}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(listRequestSpec{}, "Execute")) + }) + } +} diff --git a/internal/cmd/beta/edge/instance/update/update.go b/internal/cmd/beta/edge/instance/update/update.go new file mode 100755 index 000000000..2a6d3f6dd --- /dev/null +++ b/internal/cmd/beta/edge/instance/update/update.go @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-sdk-go/services/edge" + "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" +) + +// Struct to model user input (arguments and/or flags) +type inputModel struct { + *globalflags.GlobalFlagModel + identifier *commonValidation.Identifier + Description *string + PlanId *string +} + +// updateRequestSpec captures the details of the request for testing. +type updateRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + InstanceId string // Set if updating by ID + InstanceName string // Set if updating by Name + Payload edge.UpdateInstancePayload + + // Execute is a closure that wraps the actual SDK call + Execute func() error +} + +// OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. +// InstanceWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. +type instanceWaiter interface { + WaitWithContext(context.Context) (*edge.Instance, error) +} + +// A function that creates an instance waiter +type instanceWaiterFactory = func(client *edge.APIClient) instanceWaiter + +// Command constructor +// Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags +// instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname +// is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we +// won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates an edge instance", + Long: "Updates a STACKIT Edge Cloud (STEC) instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + fmt.Sprintf(`Updates the description of an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance update --%s "xxx" --%s "yyy"`, commonInstance.InstanceIdFlag, commonInstance.DescriptionFlag)), + examples.NewExample( + fmt.Sprintf(`Updates the plan of an edge instance with %s "xxx"`, commonInstance.DisplayNameFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance update --%s "xxx" --%s "yyy"`, commonInstance.DisplayNameFlag, commonInstance.PlanIdFlag)), + examples.NewExample( + fmt.Sprintf(`Updates the description and plan of an edge instance with %s "xxx"`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud instance update --%s "xxx" --%s "yyy" --%s "zzz"`, commonInstance.InstanceIdFlag, commonInstance.DescriptionFlag, commonInstance.PlanIdFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + // If project label can't be determined, fall back to project ID + projectLabel = model.ProjectId + } + + // Prompt for confirmation + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update the edge instance %q of project %q?", model.identifier.Value, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Call API + err = run(ctx, model, apiClient) + if err != nil { + return err + } + + // Wait for async operation, if async mode not enabled + operationState := "Triggered update of" + if !model.Async { + // Wait for async operation, if async mode not enabled + // Show spinner while waiting + err := spinner.Run(params.Printer, "Updating instance", func() error { + // Determine identifier and waiter to use + waiterFactory, err := getWaiterFactory(ctx, model) + if err != nil { + return err + } + // The waiter handler needs a concrete concreteClient type. We can safely cast here as the real implementation will always match. + concreteClient, ok := apiClient.(*edge.APIClient) + if !ok { + return fmt.Errorf("failed to configure API concreteClient") + } + waiter := waiterFactory(concreteClient) + _, err = waiter.WaitWithContext(ctx) + return err + }) + + if err != nil { + return fmt.Errorf("wait for edge instance update: %w", err) + } + operationState = "Updated" + } + + params.Printer.Info("%s instance with %q %q of project %q.\n", operationState, model.identifier.Flag, model.identifier.Value, projectLabel) + + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) + cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) + cmd.Flags().StringP(commonInstance.DescriptionFlag, commonInstance.DescriptionShorthand, "", commonInstance.DescriptionUsage) + cmd.Flags().StringP(commonInstance.PlanIdFlag, "", "", commonInstance.PlanIdUsage) + + identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} + cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName + cmd.MarkFlagsOneRequired(identifierFlags...) + + // Make sure at least one updatable field is provided, otherwise it would be a no-op + updatedFields := []string{commonInstance.DescriptionFlag, commonInstance.PlanIdFlag} + cmd.MarkFlagsOneRequired(updatedFields...) +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Generate input model based on chosen flags + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + // Parse and validate user input then add it to the model + id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) + if err != nil { + return nil, err + } + model.identifier = id + + if planIdValue := flags.FlagToStringPointer(p, cmd, commonInstance.PlanIdFlag); planIdValue != nil { + if err := commonInstance.ValidatePlanId(planIdValue); err != nil { + return nil, err + } + model.PlanId = planIdValue + } + + if descriptionValue := flags.FlagToStringPointer(p, cmd, commonInstance.DescriptionFlag); descriptionValue != nil { + if err := commonInstance.ValidateDescription(*descriptionValue); err != nil { + return nil, err + } + model.Description = descriptionValue + } + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) error { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + + err = spec.Execute() + if err != nil { + return cliErr.NewRequestFailedError(err) + } + + return nil +} + +// buildRequest constructs the spec that can be tested. +// It handles the logic of choosing between UpdateInstance and UpdateInstanceByName. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*updateRequestSpec, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + spec := &updateRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + Payload: edge.UpdateInstancePayload{ + Description: model.Description, + PlanId: model.PlanId, + }, + } + + // Switch the concrete client based on the identifier flag used + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + spec.InstanceId = model.identifier.Value + req := apiClient.UpdateInstance(ctx, model.ProjectId, model.Region, model.identifier.Value) + req = req.UpdateInstancePayload(spec.Payload) + spec.Execute = req.Execute + case commonInstance.DisplayNameFlag: + spec.InstanceName = model.identifier.Value + req := apiClient.UpdateInstanceByName(ctx, model.ProjectId, model.Region, model.identifier.Value) + req = req.UpdateInstanceByNamePayload(edge.UpdateInstanceByNamePayload{ + Description: spec.Payload.Description, + PlanId: spec.Payload.PlanId, + }) + spec.Execute = req.Execute + default: + return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) + } + + return spec, nil +} + +// Returns a factory function to create the appropriate waiter based on the input model. +func getWaiterFactory(ctx context.Context, model *inputModel) (instanceWaiterFactory, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + factory := func(c *edge.APIClient) instanceWaiter { + return wait.CreateOrUpdateInstanceWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) + } + return factory, nil + case commonInstance.DisplayNameFlag: + factory := func(c *edge.APIClient) instanceWaiter { + return wait.CreateOrUpdateInstanceByNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value) + } + return factory, nil + default: + return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) + } +} diff --git a/internal/cmd/beta/edge/instance/update/update_test.go b/internal/cmd/beta/edge/instance/update/update_test.go new file mode 100755 index 000000000..c5e3e92a8 --- /dev/null +++ b/internal/cmd/beta/edge/instance/update/update_test.go @@ -0,0 +1,542 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package update + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = "instance" + testDisplayName = "test" + testDescription = "new description" + testPlanId = uuid.NewString() +) + +type mockExecutable struct { + executeFails bool + executeNotFound bool + capturedUpdatePayload *edge.UpdateInstancePayload + capturedUpdateByNamePayload *edge.UpdateInstanceByNamePayload +} + +func (m *mockExecutable) Execute() error { + if m.executeFails { + return errors.New("API error") + } + if m.executeNotFound { + return &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusNotFound, + } + } + return nil +} + +func (m *mockExecutable) UpdateInstancePayload(payload edge.UpdateInstancePayload) edge.ApiUpdateInstanceRequest { + if m.capturedUpdatePayload != nil { + *m.capturedUpdatePayload = payload + } + return m +} + +func (m *mockExecutable) UpdateInstanceByNamePayload(payload edge.UpdateInstanceByNamePayload) edge.ApiUpdateInstanceByNameRequest { + if m.capturedUpdateByNamePayload != nil { + *m.capturedUpdateByNamePayload = payload + } + return m +} + +type mockAPIClient struct { + updateInstanceMock edge.ApiUpdateInstanceRequest + updateInstanceByNameMock edge.ApiUpdateInstanceByNameRequest +} + +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + if m.updateInstanceMock != nil { + return m.updateInstanceMock + } + return &mockExecutable{} +} + +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + if m.updateInstanceByNameMock != nil { + return m.updateInstanceByNameMock + } + return &mockExecutable{} +} + +// Unused methods to satisfy the interface +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + commonInstance.InstanceIdFlag: testInstanceId, + commonInstance.DescriptionFlag: testDescription, + commonInstance.PlanIdFlag: testPlanId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(false, mods...) +} + +func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(true, mods...) +} + +func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Description: &testDescription, + PlanId: &testPlanId, + } + + if useName { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.DisplayNameFlag, + Value: testDisplayName, + } + } else { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.InstanceIdFlag, + Value: testInstanceId, + } + } + + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "by id", + want: fixtureByIdInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by name", + want: fixtureByNameInputModel(), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by id and name", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "no update flags", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.DescriptionFlag) + delete(flagValues, commonInstance.PlanIdFlag) + }), + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "plan id invalid", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.PlanIdFlag] = "not-a-uuid" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + tests := []struct { + name string + args args + want *updateRequestSpec + wantErr error + }{ + { + name: "by id", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + updateInstanceMock: &mockExecutable{}, + }, + }, + want: &updateRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceId: testInstanceId, + Payload: edge.UpdateInstancePayload{ + Description: &testDescription, + PlanId: &testPlanId, + }, + }, + }, + { + name: "by name", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + updateInstanceByNameMock: &mockExecutable{}, + }, + }, + want: &updateRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceName: testDisplayName, + Payload: edge.UpdateInstancePayload{ + Description: &testDescription, + PlanId: &testPlanId, + }, + }, + }, + { + name: "no identifier", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + wantErr: &commonErr.NoIdentifierError{}, + }, + { + name: "invalid identifier", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.identifier = &commonValidation.Identifier{Flag: "unknown", Value: "val"} + }), + client: &mockAPIClient{}, + }, + wantErr: &cliErr.BuildRequestError{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + if got != nil { + if got.Execute == nil { + t.Error("expected non-nil Execute function") + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(updateRequestSpec{}, "Execute")) + } + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + args args + }{ + { + name: "update by id success", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + updateInstanceMock: &mockExecutable{}, + }, + }, + }, + { + name: "update by name success", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + updateInstanceByNameMock: &mockExecutable{}, + }, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + }, + { + name: "instance not found error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + updateInstanceMock: &mockExecutable{ + executeNotFound: true, + }, + }, + }, + }, + { + name: "update by id API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{ + updateInstanceMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + { + name: "update by name API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{ + updateInstanceByNameMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := run(testCtx, tt.args.model, tt.args.client) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} + +func TestGetWaiterFactory(t *testing.T) { + type args struct { + model *inputModel + } + + tests := []struct { + name string + wantErr error + want bool + args args + }{ + { + name: "by id", + want: true, + args: args{ + model: fixtureByIdInputModel(), + }, + }, + { + name: "by name", + want: true, + args: args{ + model: fixtureByNameInputModel(), + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + want: false, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + }, + }, + { + name: "unknown identifier", + wantErr: &commonErr.InvalidIdentifierError{}, + want: false, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier.Flag = "unknown" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getWaiterFactory(testCtx, tt.args.model) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + if tt.want && got == nil { + t.Fatal("expected non-nil waiter factory") + } + if !tt.want && got != nil { + t.Fatal("expected nil waiter factory") + } + }) + } +} diff --git a/internal/cmd/beta/edge/kubeconfig/create/create.go b/internal/cmd/beta/edge/kubeconfig/create/create.go new file mode 100755 index 000000000..7f7ee4d06 --- /dev/null +++ b/internal/cmd/beta/edge/kubeconfig/create/create.go @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package create + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + identifier *commonValidation.Identifier + DisableWriting bool + Filepath *string + Overwrite bool + Expiration uint64 + SwitchContext bool +} + +// createRequestSpec captures the details of the request for testing. +type createRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + InstanceId string + InstanceName string + Expiration int64 + + // Execute is a closure that wraps the actual SDK call + Execute func() (*edge.Kubeconfig, error) +} + +// OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. +// KubeconfigWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. +type kubeconfigWaiter interface { + WaitWithContext(context.Context) (*edge.Kubeconfig, error) +} + +// A function that creates a kubeconfig waiter +type kubeconfigWaiterFactory = func(client *edge.APIClient) kubeconfigWaiter + +// waiterFactoryProvider is an interface that provides kubeconfig waiters so we can inject different impl. while testing. +type waiterFactoryProvider interface { + getKubeconfigWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (kubeconfigWaiter, error) +} + +// productionWaiterFactoryProvider is the real implementation used in production. +// It handles the concrete client type casting required by the SDK's wait handlers. +type productionWaiterFactoryProvider struct{} + +func (p *productionWaiterFactoryProvider) getKubeconfigWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (kubeconfigWaiter, error) { + waiterFactory, err := getWaiterFactory(ctx, model) + if err != nil { + return nil, err + } + // The waiter handler needs a concrete client type. We can safely cast here as the real implementation will always match. + edgeClient, ok := apiClient.(*edge.APIClient) + if !ok { + return nil, cliErr.NewBuildRequestError("failed to configure API client", nil) + } + return waiterFactory(edgeClient), nil +} + +// waiterProvider is the package-level variable used to get the waiter. +// It is initialized with the production implementation but can be overridden in tests. +var waiterProvider waiterFactoryProvider = &productionWaiterFactoryProvider{} + +// Command constructor +// Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags +// instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname +// is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we +// won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates or updates a local kubeconfig file of an edge instance", + Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s", + "Creates or updates a local kubeconfig file of a STACKIT Edge Cloud (STEC) instance. If the config exists in the kubeconfig file, the information will be updated.", + "By default, the kubeconfig information of the edge instance is merged into the current kubeconfig file which is determined by Kubernetes client logic. If the kubeconfig file doesn't exist, a new one will be created.", + fmt.Sprintf("You can override this behavior by specifying a custom filepath with the --%s flag or disable writing with the --%s flag.", commonKubeconfig.FilepathFlag, commonKubeconfig.DisableWritingFlag), + fmt.Sprintf("An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is %d seconds.", commonKubeconfig.ExpirationSecondsDefault), + "Note: the format for the duration is , e.g. 30d for 30 days. You may not combine units."), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + fmt.Sprintf(`Create or update a kubeconfig for the edge instance with %s "xxx". If the config exists in the kubeconfig file, the information will be updated.`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx"`, commonInstance.InstanceIdFlag)), + examples.NewExample( + fmt.Sprintf(`Create or update a kubeconfig for the edge instance with %s "xxx" in a custom filepath.`, commonInstance.DisplayNameFlag), + fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx" --filepath "yyy"`, commonInstance.DisplayNameFlag)), + examples.NewExample( + fmt.Sprintf(`Get a kubeconfig for the edge instance with %s "xxx" without writing it to a file and format the output as json.`, commonInstance.DisplayNameFlag), + fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx" --disable-writing --output-format json`, commonInstance.DisplayNameFlag)), + examples.NewExample( + fmt.Sprintf(`Create a kubeconfig for the edge instance with %s "xxx". This will replace your current kubeconfig file.`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud kubeconfig create --%s "xxx" --overwrite`, commonInstance.InstanceIdFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Prompt for confirmation is handled in outputResult + + if model.Async { + return fmt.Errorf("async mode is not supported for kubeconfig create") + } + + // Call API via waiter (which handles both the API call and waiting) + kubeconfig, err := run(ctx, model, apiClient) + if err != nil { + return err + } + + // Handle file operations or output to printer + return outputResult(params.Printer, model.OutputFormat, model, kubeconfig) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) + cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) + cmd.Flags().Bool(commonKubeconfig.DisableWritingFlag, false, commonKubeconfig.DisableWritingUsage) + cmd.Flags().StringP(commonKubeconfig.FilepathFlag, commonKubeconfig.FilepathShorthand, "", commonKubeconfig.FilepathUsage) + cmd.Flags().StringP(commonKubeconfig.ExpirationFlag, commonKubeconfig.ExpirationShorthand, "", commonKubeconfig.ExpirationUsage) + cmd.Flags().Bool(commonKubeconfig.OverwriteFlag, false, commonKubeconfig.OverwriteUsage) + cmd.Flags().Bool(commonKubeconfig.SwitchContextFlag, false, commonKubeconfig.SwitchContextUsage) + + identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} + cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName + cmd.MarkFlagsOneRequired(identifierFlags...) + cmd.MarkFlagsMutuallyExclusive(commonKubeconfig.DisableWritingFlag, commonKubeconfig.FilepathFlag) // DisableWriting xor Filepath + cmd.MarkFlagsMutuallyExclusive(commonKubeconfig.DisableWritingFlag, commonKubeconfig.OverwriteFlag) // DisableWriting xor Overwrite +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Generate input model based on chosen flags + model := inputModel{ + GlobalFlagModel: globalFlags, + Filepath: flags.FlagToStringPointer(p, cmd, commonKubeconfig.FilepathFlag), + Overwrite: flags.FlagToBoolValue(p, cmd, commonKubeconfig.OverwriteFlag), + SwitchContext: flags.FlagToBoolValue(p, cmd, commonKubeconfig.SwitchContextFlag), + } + + // Parse and validate user input then add it to the model + id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) + if err != nil { + return nil, err + } + model.identifier = id + + // Parse and validate kubeconfig expiration time + if expString := flags.FlagToStringPointer(p, cmd, commonKubeconfig.ExpirationFlag); expString != nil { + expTime, err := utils.ConvertToSeconds(*expString) + if err != nil { + return nil, &cliErr.FlagValidationError{ + Flag: commonKubeconfig.ExpirationFlag, + Details: err.Error(), + } + } + if err := commonKubeconfig.ValidateExpiration(&expTime); err != nil { + return nil, &cliErr.FlagValidationError{ + Flag: commonKubeconfig.ExpirationFlag, + Details: err.Error(), + } + } + model.Expiration = expTime + } else { + // Default expiration is 1 hour + defaultExp := uint64(commonKubeconfig.ExpirationSecondsDefault) + model.Expiration = defaultExp + } + + disableWriting := flags.FlagToBoolValue(p, cmd, commonKubeconfig.DisableWritingFlag) + model.DisableWriting = disableWriting + // Make sure to only output if the format is explicitly set + if disableWriting { + if globalFlags.OutputFormat == "" || globalFlags.OutputFormat == print.NoneOutputFormat { + return nil, &cliErr.FlagValidationError{ + Flag: commonKubeconfig.DisableWritingFlag, + Details: fmt.Sprintf("must be used with --%s", globalflags.OutputFormatFlag.Name()), + } + } + if globalFlags.OutputFormat != print.JSONOutputFormat && globalFlags.OutputFormat != print.YAMLOutputFormat { + return nil, &cliErr.FlagValidationError{ + Flag: globalflags.OutputFormatFlag.Name(), + Details: fmt.Sprintf("valid output formats for this command are: %s", fmt.Sprintf("%s, %s", print.JSONOutputFormat, print.YAMLOutputFormat)), + } + } + } + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Kubeconfig, error) { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return nil, err + } + + resp, err := spec.Execute() + if err != nil { + return nil, cliErr.NewRequestFailedError(err) + } + + return resp, nil +} + +// buildRequest constructs the spec that can be tested. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*createRequestSpec, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + spec := &createRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + Expiration: int64(model.Expiration), // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe + } + + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + spec.InstanceId = model.identifier.Value + case commonInstance.DisplayNameFlag: + spec.InstanceName = model.identifier.Value + default: + return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) + } + + // Closure used to decouple the actual SDK call for easier testing + spec.Execute = func() (*edge.Kubeconfig, error) { + // Get the waiter from the provider (handles client type casting internally) + waiter, err := waiterProvider.getKubeconfigWaiter(ctx, model, apiClient) + if err != nil { + return nil, err + } + + return waiter.WaitWithContext(ctx) + } + + return spec, nil +} + +// Returns a factory function to create the appropriate waiter based on the input model. +func getWaiterFactory(ctx context.Context, model *inputModel) (kubeconfigWaiterFactory, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + // The KubeconfigWaitHandlers don't wait for the kubeconfig to be created, but for the instance to be ready to return a kubeconfig. + // Convert uint64 to int64 to match the API's type. + var expiration = int64(model.Expiration) // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + factory := func(c *edge.APIClient) kubeconfigWaiter { + return wait.KubeconfigWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) + } + return factory, nil + case commonInstance.DisplayNameFlag: + factory := func(c *edge.APIClient) kubeconfigWaiter { + return wait.KubeconfigByInstanceNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) + } + return factory, nil + default: + return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) + } +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat string, model *inputModel, kubeconfig *edge.Kubeconfig) error { + // Ensure kubeconfig data is present + if kubeconfig == nil || kubeconfig.Kubeconfig == nil { + return fmt.Errorf("no kubeconfig returned from the API") + } + kubeconfigMap := *kubeconfig.Kubeconfig + + // Determine output format for terminal or file output + var format string + switch outputFormat { + case print.JSONOutputFormat: + // JSON if explicitly requested + format = print.JSONOutputFormat + case print.YAMLOutputFormat: + // YAML if explicitly requested + format = print.YAMLOutputFormat + default: + if model.DisableWriting { + // If not explicitly requested, use JSON as default for terminal output + format = print.JSONOutputFormat + } else { + // If not explicitly requested, use YAML as default for file output + format = print.YAMLOutputFormat + } + } + + // Marshal kubeconfig data based on the determined format + kubeconfigData, err := marshalKubeconfig(kubeconfigMap, format) + if err != nil { + return err + } + + // Handle file writing and output + if !model.DisableWriting { + // Build options for writing kubeconfig + opts := commonKubeconfig.NewWriteOptions(). + WithOverwrite(model.Overwrite). + WithSwitchContext(model.SwitchContext) + + // Add confirmation callback if not assumeYes + if !model.AssumeYes { + confirmFn := func(message string) error { + return p.PromptForConfirmation(message) + } + opts = opts.WithConfirmation(confirmFn) + } + + path, err := commonKubeconfig.WriteKubeconfig(model.Filepath, kubeconfigData, opts) + if err != nil { + return err + } + + // Inform the user about the successful write operation + p.Outputf("Wrote kubeconfig for instance %q to %q.\n", model.identifier.Value, *path) + + if model.SwitchContext { + p.Outputln("Switched context as requested.") + } + } else { + p.Outputln(kubeconfigData) + } + return nil +} + +// Marshal kubeconfig data to the specified format +func marshalKubeconfig(kubeconfigMap map[string]interface{}, format string) (string, error) { + switch format { + case print.JSONOutputFormat: + kubeconfigJSON, err := json.MarshalIndent(kubeconfigMap, "", " ") + if err != nil { + return "", fmt.Errorf("marshal kubeconfig to JSON: %w", err) + } + return string(kubeconfigJSON), nil + case print.YAMLOutputFormat: + kubeconfigYAML, err := yaml.MarshalWithOptions(kubeconfigMap, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return "", fmt.Errorf("marshal kubeconfig to YAML: %w", err) + } + return string(kubeconfigYAML), nil + default: + return "", fmt.Errorf("%w: %s", commonErr.NewNoIdentifierError(""), format) + } +} diff --git a/internal/cmd/beta/edge/kubeconfig/create/create_test.go b/internal/cmd/beta/edge/kubeconfig/create/create_test.go new file mode 100755 index 000000000..051e470f8 --- /dev/null +++ b/internal/cmd/beta/edge/kubeconfig/create/create_test.go @@ -0,0 +1,820 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package create + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/goccy/go-yaml" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = "instance" + testDisplayName = "test" + testExpiration = "1h" +) + +const ( + testKubeconfig = ` +apiVersion: v1 +clusters: +- cluster: + server: https://server-1.com + name: cluster-1 +contexts: +- context: + cluster: cluster-1 + user: user-1 + name: context-1 +current-context: context-1 +kind: Config +preferences: {} +users: +- name: user-1 + user: {} +` +) + +// Helper function to create a new instance of Kubeconfig +// +//nolint:gocritic // ptrToRefParam: Required by edge.Kubeconfig API which expects *map[string]interface{} +func testKubeconfigMap() *map[string]interface{} { + var kubeconfigMap map[string]interface{} + err := yaml.Unmarshal([]byte(testKubeconfig), &kubeconfigMap) + if err != nil { + // This should never happen in tests with valid YAML + panic(err) + } + return utils.Ptr(kubeconfigMap) +} + +// mockKubeconfigWaiter is a mock for the kubeconfigWaiter interface +type mockKubeconfigWaiter struct { + waitFails bool + waitNotFound bool + waitResp *edge.Kubeconfig +} + +func (m *mockKubeconfigWaiter) WaitWithContext(_ context.Context) (*edge.Kubeconfig, error) { + if m.waitFails { + return nil, errors.New("wait error") + } + if m.waitNotFound { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusNotFound, + } + } + if m.waitResp != nil { + return m.waitResp, nil + } + + // Default kubeconfig response + return &edge.Kubeconfig{ + Kubeconfig: testKubeconfigMap(), + }, nil +} + +// testWaiterFactoryProvider is a test implementation that returns mock waiters. +type testWaiterFactoryProvider struct { + waiter kubeconfigWaiter +} + +func (t *testWaiterFactoryProvider) getKubeconfigWaiter(_ context.Context, model *inputModel, _ client.APIClient) (kubeconfigWaiter, error) { + if model == nil || model.identifier == nil { + return nil, &commonErr.NoIdentifierError{} + } + + // Validate identifier like the real implementation + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag: + // Return our mock waiter directly, bypassing the client type casting issue + return t.waiter, nil + default: + return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) + } +} + +// mockAPIClient is a mock for the edge.APIClient interface +type mockAPIClient struct{} + +// Unused methods to satisfy the interface +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} + +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} + +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} + +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} + +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} + +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} + +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} + +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} + +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} + +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + commonInstance.InstanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(false, mods...) +} + +func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(true, mods...) +} + +func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisableWriting: false, + Filepath: nil, + Overwrite: false, + Expiration: uint64(3600), // Default 1 hour + SwitchContext: false, + } + + if useName { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.DisplayNameFlag, + Value: testDisplayName, + } + } else { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.InstanceIdFlag, + Value: testInstanceId, + } + } + + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "by id", + want: fixtureByIdInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by name", + want: fixtureByNameInputModel(), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "with expiration", + want: fixtureByIdInputModel(func(model *inputModel) { + model.Expiration = uint64(3600) + }), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = testExpiration + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by id and name", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "instance id missing", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + }), + }, + }, + { + name: "instance id empty", + wantErr: "id may not be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "" + }), + }, + }, + { + name: "instance id too long", + wantErr: "id is too long", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" + }), + }, + }, + { + name: "instance id too short", + wantErr: "id is too short", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "id" + }), + }, + }, + { + name: "name too short", + wantErr: "name is too short", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foo" + }), + }, + }, + { + name: "name too long", + wantErr: "name is too long", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" + }), + }, + }, + { + name: "disable writing and invalid output format", + wantErr: "valid output formats for this command are", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.DisableWritingFlag] = "true" + flagValues[globalflags.OutputFormatFlag.Name()] = print.PrettyOutputFormat + }), + }, + }, + { + name: "disable writing and default output format", + wantErr: "must be used with --output-format", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.DisableWritingFlag] = "true" + }), + }, + }, + { + name: "disable writing and valid output format", + want: fixtureByIdInputModel(func(model *inputModel) { + model.DisableWriting = true + model.OutputFormat = print.YAMLOutputFormat + }), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.DisableWritingFlag] = "true" + flagValues[globalflags.OutputFormatFlag.Name()] = print.YAMLOutputFormat + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "invalid expiration format", + wantErr: "invalid time string format", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = "invalid" + }), + }, + }, + { + name: "expiration too short", + wantErr: "expiration is too small", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = "1s" + }), + }, + }, + { + name: "expiration too long", + wantErr: "expiration is too large", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = "13M" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + waiter kubeconfigWaiter + } + + tests := []struct { + name string + wantErr error + args args + }{ + { + name: "run by id success", + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{}, + }, + }, + { + name: "run by name success", + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{}, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{}, + }, + }, + { + name: "instance not found error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{waitNotFound: true}, + }, + }, + { + name: "get kubeconfig by id API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{waitFails: true}, + }, + }, + { + name: "get kubeconfig by name API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{waitFails: true}, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + waiter: &mockKubeconfigWaiter{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Override production waiterProvider package level variable for testing + prodWaiterProvider := waiterProvider + waiterProvider = &testWaiterFactoryProvider{waiter: tt.args.waiter} + defer func() { waiterProvider = prodWaiterProvider }() + + _, err := run(testCtx, tt.args.model, tt.args.client) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *createRequestSpec + args args + }{ + { + name: "by id", + want: &createRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceId: testInstanceId, + Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), + }, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + }, + }, + { + name: "by name", + want: &createRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceName: testDisplayName, + Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), + }, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{}, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(createRequestSpec{}, "Execute")) + }) + } +} + +func TestGetWaiterFactory(t *testing.T) { + type args struct { + model *inputModel + } + + tests := []struct { + name string + wantErr error + want bool + args args + }{ + { + name: "by id", + want: true, + args: args{ + model: fixtureByIdInputModel(), + }, + }, + { + name: "by name", + want: true, + args: args{ + model: fixtureByNameInputModel(), + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + want: false, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + }, + }, + { + name: "unknown identifier", + wantErr: &commonErr.InvalidIdentifierError{}, + want: false, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier.Flag = "unknown" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getWaiterFactory(testCtx, tt.args.model) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + if tt.want && got == nil { + t.Fatal("expected non-nil waiter factory") + } + if !tt.want && got != nil { + t.Fatal("expected nil waiter factory") + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + kubeconfig *edge.Kubeconfig + } + + tests := []struct { + name string + wantErr any + args args + }{ + { + name: "no kubeconfig", + wantErr: true, + args: args{ + model: fixtureByIdInputModel(), + kubeconfig: nil, + }, + }, + { + name: "kubeconfig with nil kubeconfig data", + wantErr: true, + args: args{ + model: fixtureByIdInputModel(), + kubeconfig: &edge.Kubeconfig{Kubeconfig: nil}, + }, + }, + { + name: "output json with disable writing", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + model.DisableWriting = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "output yaml with disable writing", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + model.DisableWriting = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "output default with disable writing", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.DisableWriting = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "output by name with json format and disable writing", + args: args{ + model: fixtureByNameInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + model.DisableWriting = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "output by name with yaml format and disable writing", + args: args{ + model: fixtureByNameInputModel(func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + model.DisableWriting = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "output by name default with disable writing", + args: args{ + model: fixtureByNameInputModel(func(model *inputModel) { + model.DisableWriting = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "file writing enabled (default behavior)", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.AssumeYes = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "file writing with overwrite enabled", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.Overwrite = true + model.AssumeYes = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + { + name: "file writing with switch context enabled", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.SwitchContext = true + model.AssumeYes = true + }), + kubeconfig: &edge.Kubeconfig{Kubeconfig: testKubeconfigMap()}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.args.model.OutputFormat, tt.args.model, tt.args.kubeconfig) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} diff --git a/internal/cmd/beta/edge/kubeconfig/kubeconfig.go b/internal/cmd/beta/edge/kubeconfig/kubeconfig.go new file mode 100644 index 000000000..b44c2e1a4 --- /dev/null +++ b/internal/cmd/beta/edge/kubeconfig/kubeconfig.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package kubeconfig + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/kubeconfig/create" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "kubeconfig", + Short: "Provides functionality for edge kubeconfig.", + Long: "Provides functionality for STACKIT Edge Cloud (STEC) kubeconfig management.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) +} diff --git a/internal/cmd/beta/edge/plans/list/list.go b/internal/cmd/beta/edge/plans/list/list.go new file mode 100755 index 000000000..e8b8607fd --- /dev/null +++ b/internal/cmd/beta/edge/plans/list/list.go @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// User input struct for the command +const ( + limitFlag = "limit" +) + +// Struct to model user input (arguments and/or flags) +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +// listRequestSpec captures the details of the request for testing. +type listRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Limit *int64 + + // Execute is a closure that wraps the actual SDK call + Execute func() (*edge.PlanList, error) +} + +// Command constructor +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists available edge service plans", + Long: "Lists available STACKIT Edge Cloud (STEC) service plans of a project", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists all edge plans for a given project`, + `$ stackit beta edge-cloud plan list`), + examples.NewExample( + `Lists all edge plans for a given project and limits the output to two plans`, + fmt.Sprintf(`$ stackit beta edge-cloud plan list --%s 2`, limitFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + // If project label can't be determined, fall back to project ID + projectLabel = model.ProjectId + } + + // Call API + resp, err := run(ctx, model, apiClient) + if err != nil { + return err + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Parse and validate user input then add it to the model + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) ([]edge.Plan, error) { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return nil, err + } + + resp, err := spec.Execute() + if err != nil { + return nil, cliErr.NewRequestFailedError(err) + } + if resp == nil { + return nil, fmt.Errorf("list plans: empty response from API") + } + if resp.ValidPlans == nil { + return nil, fmt.Errorf("list plans: valid plans missing in response") + } + plans := *resp.ValidPlans + + // Truncate output + if spec.Limit != nil && len(plans) > int(*spec.Limit) { + plans = plans[:*spec.Limit] + } + + return plans, nil +} + +// buildRequest constructs the spec that can be tested. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*listRequestSpec, error) { + req := apiClient.ListPlansProject(ctx, model.ProjectId) + + return &listRequestSpec{ + ProjectID: model.ProjectId, + Limit: model.Limit, + Execute: req.Execute, + }, nil +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []edge.Plan) error { + return p.OutputResult(outputFormat, plans, func() error { + // No plans found for project + if len(plans) == 0 { + p.Outputf("No plans found for project %q\n", projectLabel) + return nil + } + + // Display plans found for project in a table + table := tables.NewTable() + // List: only output the most important fields. Be sure to filter for any non-required fields. + table.SetHeader("ID", "NAME", "DESCRIPTION", "MAX EDGE HOSTS") + for i := range plans { + plan := plans[i] + table.AddRow( + utils.PtrString(plan.Id), + utils.PtrString(plan.Name), + utils.PtrString(plan.Description), + utils.PtrString(plan.MaxEdgeHosts)) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/edge/plans/list/list_test.go b/internal/cmd/beta/edge/plans/list/list_test.go new file mode 100755 index 000000000..5684a69bf --- /dev/null +++ b/internal/cmd/beta/edge/plans/list/list_test.go @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package list + +import ( + "context" + "errors" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" +) + +// mockExecutable is a mock for the Executable interface +type mockExecutable struct { + executeFails bool + executeResp *edge.PlanList +} + +func (m *mockExecutable) Execute() (*edge.PlanList, error) { + if m.executeFails { + return nil, errors.New("API error") + } + + if m.executeResp != nil { + return m.executeResp, nil + } + return &edge.PlanList{ + ValidPlans: &[]edge.Plan{ + {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, + {Id: utils.Ptr("plan-2"), Name: utils.Ptr("Premium")}, + }, + }, nil +} + +// mockAPIClient is a mock for the edge.APIClient interface +type mockAPIClient struct { + getPlansMock edge.ApiListPlansProjectRequest +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + if m.getPlansMock != nil { + return m.getPlansMock + } + return &mockExecutable{} +} + +// Unused methods to satisfy the interface +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} + +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} + +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "list success", + want: fixtureInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "limit invalid value", + wantErr: "invalid syntax", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + }, + }, + { + name: "limit is zero", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + }, + }, + { + name: "limit is negative", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-0" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want []edge.Plan + args args + }{ + { + name: "list success", + want: []edge.Plan{ + {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, + {Id: utils.Ptr("plan-2"), Name: utils.Ptr("Premium")}, + }, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{}, + }, + }, + { + name: "list success with limit", + want: []edge.Plan{ + {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, + }, + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(1)) + }), + client: &mockAPIClient{}, + }, + }, + { + name: "list success with limit greater than items", + want: []edge.Plan{ + {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, + {Id: utils.Ptr("plan-2"), Name: utils.Ptr("Premium")}, + }, + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(int64(5)) + }), + client: &mockAPIClient{}, + }, + }, + { + name: "list success with no items", + want: []edge.Plan{}, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + getPlansMock: &mockExecutable{ + executeResp: &edge.PlanList{ValidPlans: &[]edge.Plan{}}, + }, + }, + }, + }, + { + name: "list API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + getPlansMock: &mockExecutable{ + executeFails: true, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := run(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + plans []edge.Plan + projectLabel string + } + + tests := []struct { + name string + wantErr error + args args + }{ + { + name: "output json", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + plans: []edge.Plan{ + {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, + }, + projectLabel: "test-project", + }, + }, + { + name: "output yaml", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + }), + plans: []edge.Plan{ + {Id: utils.Ptr("plan-1"), Name: utils.Ptr("Standard")}, + }, + projectLabel: "test-project", + }, + }, + { + name: "output default with plans", + args: args{ + model: fixtureInputModel(), + plans: []edge.Plan{ + { + Id: utils.Ptr("plan-1"), + Name: utils.Ptr("Standard"), + Description: utils.Ptr("Standard plan description"), + }, + { + Id: utils.Ptr("plan-2"), + Name: utils.Ptr("Premium"), + Description: utils.Ptr("Premium plan description"), + }, + }, + projectLabel: "test-project", + }, + }, + { + name: "output default with no plans", + args: args{ + model: fixtureInputModel(), + plans: []edge.Plan{}, + projectLabel: "test-project", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.args.model.OutputFormat, tt.args.projectLabel, tt.args.plans) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *listRequestSpec + args args + }{ + { + name: "success", + want: &listRequestSpec{ + ProjectID: testProjectId, + }, + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Limit = nil + }), + client: &mockAPIClient{ + getPlansMock: &mockExecutable{}, + }, + }, + }, + { + name: "success with limit", + want: &listRequestSpec{ + ProjectID: testProjectId, + Limit: utils.Ptr(int64(10)), + }, + args: args{ + model: fixtureInputModel(), + client: &mockAPIClient{ + getPlansMock: &mockExecutable{}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(listRequestSpec{}, "Execute")) + }) + } +} diff --git a/internal/cmd/beta/edge/plans/plans.go b/internal/cmd/beta/edge/plans/plans.go new file mode 100644 index 000000000..d5ccb0721 --- /dev/null +++ b/internal/cmd/beta/edge/plans/plans.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package plans + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/plans/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "plans", + Short: "Provides functionality for edge service plans.", + Long: "Provides functionality for STACKIT Edge Cloud (STEC) service plan management.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/edge/token/create/create.go b/internal/cmd/beta/edge/token/create/create.go new file mode 100755 index 000000000..cbe7ab900 --- /dev/null +++ b/internal/cmd/beta/edge/token/create/create.go @@ -0,0 +1,292 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + "github.com/stackitcloud/stackit-sdk-go/services/edge/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + identifier *commonValidation.Identifier + Expiration uint64 +} + +// createRequestSpec captures the details of the request for testing. +type createRequestSpec struct { + // Exported fields allow tests to inspect the request inputs + ProjectID string + Region string + InstanceId string + InstanceName string + Expiration int64 + + // Execute is a closure that wraps the actual SDK call + Execute func() (*edge.Token, error) +} + +// OpenApi generated code will have different types for by-instance-id and by-display-name API calls and therefore different wait handlers. +// tokenWaiter is an interface to abstract the different wait handlers so they can be used interchangeably. +type tokenWaiter interface { + WaitWithContext(context.Context) (*edge.Token, error) +} + +// A function that creates a token waiter +type tokenWaiterFactory = func(client *edge.APIClient) tokenWaiter + +// waiterFactoryProvider is an interface that provides token waiters so we can inject different impl. while testing. +type waiterFactoryProvider interface { + getTokenWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (tokenWaiter, error) +} + +// productionWaiterFactoryProvider is the real implementation used in production. +// It handles the concrete client type casting required by the SDK's wait handlers. +type productionWaiterFactoryProvider struct{} + +func (p *productionWaiterFactoryProvider) getTokenWaiter(ctx context.Context, model *inputModel, apiClient client.APIClient) (tokenWaiter, error) { + waiterFactory, err := getWaiterFactory(ctx, model) + if err != nil { + return nil, err + } + // The waiter handler needs a concrete client type. We can safely cast here as the real implementation will always match. + edgeClient, ok := apiClient.(*edge.APIClient) + if !ok { + return nil, cliErr.NewBuildRequestError("failed to configure API client", nil) + } + return waiterFactory(edgeClient), nil +} + +// waiterProvider is the package-level variable used to get the waiter. +// It is initialized with the production implementation but can be overridden in tests. +var waiterProvider waiterFactoryProvider = &productionWaiterFactoryProvider{} + +// Command constructor +// Instance id and displayname are likely to be refactored in future. For the time being we decided to use flags +// instead of args to provide the instance-id xor displayname to uniquely identify an instance. The displayname +// is guaranteed to be unique within a given project as of today. The chosen flag over args approach ensures we +// won't need a breaking change of the CLI when we refactor the commands to take the identifier as arg at some point. +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a token for an edge instance", + Long: fmt.Sprintf("%s\n\n%s\n%s", + "Creates a token for a STACKIT Edge Cloud (STEC) instance.", + fmt.Sprintf("An expiration time can be set for the token. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is %d seconds.", commonKubeconfig.ExpirationSecondsDefault), + "Note: the format for the duration is , e.g. 30d for 30 days. You may not combine units."), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + fmt.Sprintf(`Create a token for the edge instance with %s "xxx".`, commonInstance.InstanceIdFlag), + fmt.Sprintf(`$ stackit beta edge-cloud token create --%s "xxx"`, commonInstance.InstanceIdFlag)), + examples.NewExample( + fmt.Sprintf(`Create a token for the edge instance with %s "xxx". The token will be valid for one day.`, commonInstance.DisplayNameFlag), + fmt.Sprintf(`$ stackit beta edge-cloud token create --%s "xxx" --expiration 1d`, commonInstance.DisplayNameFlag)), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + + // Parse user input (arguments and/or flags) + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + if model.Async { + return fmt.Errorf("async mode is not supported for token create") + } + + // Call API + resp, err := run(ctx, model, apiClient) + if err != nil { + return err + } + + // Handle output to printer + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(commonInstance.InstanceIdFlag, commonInstance.InstanceIdShorthand, "", commonInstance.InstanceIdUsage) + cmd.Flags().StringP(commonInstance.DisplayNameFlag, commonInstance.DisplayNameShorthand, "", commonInstance.DisplayNameUsage) + cmd.Flags().StringP(commonKubeconfig.ExpirationFlag, commonKubeconfig.ExpirationShorthand, "", commonKubeconfig.ExpirationUsage) + + identifierFlags := []string{commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag} + cmd.MarkFlagsMutuallyExclusive(identifierFlags...) // InstanceId xor DisplayName + cmd.MarkFlagsOneRequired(identifierFlags...) +} + +// Parse user input (arguments and/or flags) +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // Generate input model based on chosen flags + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + // Parse and validate user input then add it to the model + id, err := commonValidation.GetValidatedInstanceIdentifier(p, cmd) + if err != nil { + return nil, err + } + model.identifier = id + + // Parse and validate kubeconfig expiration time + if expString := flags.FlagToStringPointer(p, cmd, commonKubeconfig.ExpirationFlag); expString != nil { + expTime, err := utils.ConvertToSeconds(*expString) + if err != nil { + return nil, &cliErr.FlagValidationError{ + Flag: commonKubeconfig.ExpirationFlag, + Details: err.Error(), + } + } + if err := commonKubeconfig.ValidateExpiration(&expTime); err != nil { + return nil, &cliErr.FlagValidationError{ + Flag: commonKubeconfig.ExpirationFlag, + Details: err.Error(), + } + } + model.Expiration = expTime + } else { + // Default expiration is 1 hour + defaultExp := uint64(commonKubeconfig.ExpirationSecondsDefault) + model.Expiration = defaultExp + } + + // Make sure to only output if the format is not none + if globalFlags.OutputFormat == print.NoneOutputFormat { + return nil, &cliErr.FlagValidationError{ + Flag: globalflags.OutputFormatFlag.Name(), + Details: fmt.Sprintf("valid formats for this command are: %s", fmt.Sprintf("%s, %s, %s", print.PrettyOutputFormat, print.JSONOutputFormat, print.YAMLOutputFormat)), + } + } + + // Log the parsed model if --verbosity is set to debug + p.DebugInputModel(model) + return &model, nil +} + +// Run is the main execution function used by the command runner. +// It is decoupled from TTY output to have the ability to mock the API client during testing. +func run(ctx context.Context, model *inputModel, apiClient client.APIClient) (*edge.Token, error) { + spec, err := buildRequest(ctx, model, apiClient) + if err != nil { + return nil, err + } + + resp, err := spec.Execute() + if err != nil { + return nil, cliErr.NewRequestFailedError(err) + } + + return resp, nil +} + +// buildRequest constructs the spec that can be tested. +func buildRequest(ctx context.Context, model *inputModel, apiClient client.APIClient) (*createRequestSpec, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + spec := &createRequestSpec{ + ProjectID: model.ProjectId, + Region: model.Region, + Expiration: int64(model.Expiration), // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe + } + + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + spec.InstanceId = model.identifier.Value + case commonInstance.DisplayNameFlag: + spec.InstanceName = model.identifier.Value + default: + return nil, fmt.Errorf("%w: %w", cliErr.NewBuildRequestError("invalid identifier flag", nil), commonErr.NewInvalidIdentifierError(model.identifier.Flag)) + } + + // Closure used to decouple the actual SDK call for easier testing + spec.Execute = func() (*edge.Token, error) { + // Get the waiter from the provider (handles client type casting internally) + waiter, err := waiterProvider.getTokenWaiter(ctx, model, apiClient) + if err != nil { + return nil, err + } + + return waiter.WaitWithContext(ctx) + } + + return spec, nil +} + +// Returns a factory function to create the appropriate waiter based on the input model. +func getWaiterFactory(ctx context.Context, model *inputModel) (tokenWaiterFactory, error) { + if model == nil || model.identifier == nil { + return nil, commonErr.NewNoIdentifierError("") + } + + // The tokenWaitHandlers don't wait for the token to be created, but for the instance to be ready to return a token. + // Convert uint64 to int64 to match the API's type. + var expiration = int64(model.Expiration) // #nosec G115 ValidateExpiration ensures safe bounds, conversion is safe + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag: + factory := func(c *edge.APIClient) tokenWaiter { + return wait.TokenWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) + } + return factory, nil + case commonInstance.DisplayNameFlag: + factory := func(c *edge.APIClient) tokenWaiter { + return wait.TokenByInstanceNameWaitHandler(ctx, c, model.ProjectId, model.Region, model.identifier.Value, &expiration) + } + return factory, nil + default: + return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) + } +} + +// Output result based on the configured output format +func outputResult(p *print.Printer, outputFormat string, token *edge.Token) error { + if token == nil || token.Token == nil { + // This is only to prevent nil pointer deref. + // As long as the API behaves as defined by it's spec, instance can not be empty (HTTP 200 with an empty body) + return fmt.Errorf("no token returned from the API") + } + tokenString := *token.Token + + return p.OutputResult(outputFormat, token, func() error { + p.Outputln(tokenString) + return nil + }) +} diff --git a/internal/cmd/beta/edge/token/create/create_test.go b/internal/cmd/beta/edge/token/create/create_test.go new file mode 100755 index 000000000..8f6b69404 --- /dev/null +++ b/internal/cmd/beta/edge/token/create/create_test.go @@ -0,0 +1,677 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package create + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/client" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + commonKubeconfig "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/kubeconfig" + commonValidation "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/validation" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = "instance" + testDisplayName = "test" + testExpiration = "1h" +) + +// mockTokenWaiter is a mock for the tokenWaiter interface +type mockTokenWaiter struct { + waitFails bool + waitNotFound bool + waitResp *edge.Token +} + +func (m *mockTokenWaiter) WaitWithContext(_ context.Context) (*edge.Token, error) { + if m.waitFails { + return nil, errors.New("wait error") + } + if m.waitNotFound { + return nil, &oapierror.GenericOpenAPIError{ + StatusCode: http.StatusNotFound, + } + } + if m.waitResp != nil { + return m.waitResp, nil + } + + // Default token response + tokenString := "test-token-string" + return &edge.Token{ + Token: &tokenString, + }, nil +} + +// testWaiterFactoryProvider is a test implementation that returns mock waiters. +type testWaiterFactoryProvider struct { + waiter tokenWaiter +} + +func (t *testWaiterFactoryProvider) getTokenWaiter(_ context.Context, model *inputModel, _ client.APIClient) (tokenWaiter, error) { + if model == nil || model.identifier == nil { + return nil, &commonErr.NoIdentifierError{} + } + + // Validate identifier like the real implementation + switch model.identifier.Flag { + case commonInstance.InstanceIdFlag, commonInstance.DisplayNameFlag: + // Return our mock waiter directly, bypassing the client type casting issue + return t.waiter, nil + default: + return nil, commonErr.NewInvalidIdentifierError(model.identifier.Flag) + } +} + +// mockAPIClient is a mock for the edge.APIClient interface +type mockAPIClient struct{} + +// Unused methods to satisfy the interface +func (m *mockAPIClient) GetTokenByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceIdRequest { + return nil +} + +func (m *mockAPIClient) GetTokenByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetTokenByInstanceNameRequest { + return nil +} + +func (m *mockAPIClient) ListPlansProject(_ context.Context, _ string) edge.ApiListPlansProjectRequest { + return nil +} + +func (m *mockAPIClient) CreateInstance(_ context.Context, _, _ string) edge.ApiCreateInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstance(_ context.Context, _, _, _ string) edge.ApiGetInstanceRequest { + return nil +} +func (m *mockAPIClient) GetInstanceByName(_ context.Context, _, _, _ string) edge.ApiGetInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) ListInstances(_ context.Context, _, _ string) edge.ApiListInstancesRequest { + return nil +} +func (m *mockAPIClient) UpdateInstance(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceRequest { + return nil +} +func (m *mockAPIClient) UpdateInstanceByName(_ context.Context, _, _, _ string) edge.ApiUpdateInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) DeleteInstance(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceRequest { + return nil +} +func (m *mockAPIClient) DeleteInstanceByName(_ context.Context, _, _, _ string) edge.ApiDeleteInstanceByNameRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceId(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceIdRequest { + return nil +} +func (m *mockAPIClient) GetKubeconfigByInstanceName(_ context.Context, _, _, _ string) edge.ApiGetKubeconfigByInstanceNameRequest { + return nil +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + commonInstance.InstanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureByIdInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(false, mods...) +} + +func fixtureByNameInputModel(mods ...func(model *inputModel)) *inputModel { + return fixtureInputModel(true, mods...) +} + +func fixtureInputModel(useName bool, mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Expiration: uint64(commonKubeconfig.ExpirationSecondsDefault), // Default 1 hour + } + + if useName { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.DisplayNameFlag, + Value: testDisplayName, + } + } else { + model.identifier = &commonValidation.Identifier{ + Flag: commonInstance.InstanceIdFlag, + Value: testInstanceId, + } + } + + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + type args struct { + flags map[string]string + cmpOpts []testUtils.ValueComparisonOption + } + + tests := []struct { + name string + wantErr any + want *inputModel + args args + }{ + { + name: "by id", + want: fixtureByIdInputModel(), + args: args{ + flags: fixtureFlagValues(), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by name", + want: fixtureByNameInputModel(), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "with expiration", + want: fixtureByIdInputModel(func(model *inputModel) { + model.Expiration = uint64(3600) + }), + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = testExpiration + }), + cmpOpts: []testUtils.ValueComparisonOption{ + testUtils.WithAllowUnexported(inputModel{}), + }, + }, + }, + { + name: "by id and name", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.DisplayNameFlag] = testDisplayName + }), + }, + }, + { + name: "no flag values", + wantErr: true, + args: args{ + flags: map[string]string{}, + }, + }, + { + name: "project id missing", + wantErr: &cliErr.ProjectIdError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + }, + }, + { + name: "project id empty", + wantErr: "value cannot be empty", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + }, + }, + { + name: "project id invalid", + wantErr: "invalid UUID length", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + }, + }, + { + name: "instance id missing", + wantErr: true, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + }), + }, + }, + { + name: "instance id empty", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "" + }), + }, + }, + { + name: "instance id too long", + wantErr: &cliErr.FlagValidationError{}, + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "invalid-instance-id" + }), + }, + }, + { + name: "instance id too short", + wantErr: "id is too short", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonInstance.InstanceIdFlag] = "id" + }), + }, + }, + { + name: "name too short", + wantErr: "name is too short", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foo" + }), + }, + }, + { + name: "name too long", + wantErr: "name is too long", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commonInstance.InstanceIdFlag) + flagValues[commonInstance.DisplayNameFlag] = "foofoofoo" + }), + }, + }, + { + name: "invalid expiration format", + wantErr: "invalid time string format", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = "invalid" + }), + }, + }, + { + name: "expiration too short", + wantErr: "expiration is too small", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = "1s" + }), + }, + }, + { + name: "expiration too long", + wantErr: "expiration is too large", + args: args{ + flags: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[commonKubeconfig.ExpirationFlag] = "13M" + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caseOpts := []testUtils.ParseInputCaseOption{} + if len(tt.args.cmpOpts) > 0 { + caseOpts = append(caseOpts, testUtils.WithParseInputCmpOptions(tt.args.cmpOpts...)) + } + + testUtils.RunParseInputCase(t, testUtils.ParseInputTestCase[*inputModel]{ + Name: tt.name, + Flags: tt.args.flags, + WantModel: tt.want, + WantErr: tt.wantErr, + CmdFactory: NewCmd, + ParseInputFunc: func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, + }, caseOpts...) + }) + } +} + +func TestRun(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + waiter tokenWaiter + } + tests := []struct { + name string + wantErr any + wantToken bool + args args + }{ + { + name: "run by id success", + wantToken: true, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{}, + }, + }, + { + name: "run by name success", + wantToken: true, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{}, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{}, + }, + }, + { + name: "instance not found error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{waitNotFound: true}, + }, + }, + { + name: "get token by id API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{waitFails: true}, + }, + }, + { + name: "get token by name API error", + wantErr: &cliErr.RequestFailedError{}, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{waitFails: true}, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + waiter: &mockTokenWaiter{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Override production waiterProvider package level variable for testing + prodWaiterProvider := waiterProvider + waiterProvider = &testWaiterFactoryProvider{waiter: tt.args.waiter} + defer func() { waiterProvider = prodWaiterProvider }() + + got, err := run(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + if tt.wantToken && got == nil { + t.Fatal("expected non-nil token") + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + type args struct { + model *inputModel + client client.APIClient + } + + tests := []struct { + name string + wantErr error + want *createRequestSpec + args args + }{ + { + name: "by id", + want: &createRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceId: testInstanceId, + Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), + }, + args: args{ + model: fixtureByIdInputModel(), + client: &mockAPIClient{}, + }, + }, + { + name: "by name", + want: &createRequestSpec{ + ProjectID: testProjectId, + Region: testRegion, + InstanceName: testDisplayName, + Expiration: int64(commonKubeconfig.ExpirationSecondsDefault), + }, + args: args{ + model: fixtureByNameInputModel(), + client: &mockAPIClient{}, + }, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + }), + client: &mockAPIClient{}, + }, + }, + { + name: "identifier invalid", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{ + model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = &commonValidation.Identifier{ + Flag: "unknown-flag", + Value: "some-value", + } + }), + client: &mockAPIClient{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := buildRequest(testCtx, tt.args.model, tt.args.client) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + testUtils.AssertValue(t, got, tt.want, testUtils.WithIgnoreFields(createRequestSpec{}, "Execute")) + }) + } +} + +func TestGetWaiterFactory(t *testing.T) { + type args struct { + model *inputModel + } + tests := []struct { + name string + want bool + wantErr error + args args + }{ + { + name: "by id", + want: true, + args: args{model: fixtureByIdInputModel()}, + }, + { + name: "by name", + want: true, + args: args{model: fixtureByNameInputModel()}, + }, + { + name: "no id or name", + wantErr: &commonErr.NoIdentifierError{}, + args: args{model: fixtureInputModel(false, func(model *inputModel) { + model.identifier = nil + })}, + }, + { + name: "unknown identifier", + wantErr: &commonErr.InvalidIdentifierError{}, + args: args{model: fixtureInputModel(false, func(model *inputModel) { + model.identifier.Flag = "unknown" + })}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getWaiterFactory(testCtx, tt.args.model) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + if tt.want && got == nil { + t.Fatal("expected non-nil waiter factory") + } + if !tt.want && got != nil { + t.Fatal("expected nil waiter factory") + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + token *edge.Token + } + tests := []struct { + name string + wantErr any + args args + }{ + { + name: "default output format", + args: args{ + model: fixtureByIdInputModel(), + token: &edge.Token{ + Token: func() *string { s := "test-token"; return &s }(), + }, + }, + }, + { + name: "JSON output format", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + token: &edge.Token{ + Token: func() *string { s := "test-token"; return &s }(), + }, + }, + }, + { + name: "YAML output format", + args: args{ + model: fixtureByIdInputModel(func(model *inputModel) { + model.OutputFormat = print.YAMLOutputFormat + }), + token: &edge.Token{ + Token: func() *string { s := "test-token"; return &s }(), + }, + }, + }, + { + name: "nil token", + wantErr: true, + args: args{ + model: fixtureByIdInputModel(), + token: nil, + }, + }, + { + name: "nil token string", + wantErr: true, + args: args{ + model: fixtureByIdInputModel(), + token: &edge.Token{Token: nil}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + + err := outputResult(params.Printer, tt.args.model.OutputFormat, tt.args.token) + testUtils.AssertError(t, err, tt.wantErr) + }) + } +} diff --git a/internal/cmd/beta/edge/token/token.go b/internal/cmd/beta/edge/token/token.go new file mode 100644 index 000000000..8fd725a72 --- /dev/null +++ b/internal/cmd/beta/edge/token/token.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package token + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/edge/token/create" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "token", + Short: "Provides functionality for edge service token.", + Long: "Provides functionality for STACKIT Edge Cloud (STEC) token management.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) +} diff --git a/internal/cmd/beta/intake/create/create.go b/internal/cmd/beta/intake/create/create.go new file mode 100644 index 000000000..9cb6c1fb7 --- /dev/null +++ b/internal/cmd/beta/intake/create/create.go @@ -0,0 +1,248 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + displayNameFlag = "display-name" + runnerIdFlag = "runner-id" + descriptionFlag = "description" + labelsFlag = "labels" + catalogURIFlag = "catalog-uri" + catalogWarehouseFlag = "catalog-warehouse" + catalogNamespaceFlag = "catalog-namespace" + catalogTableNameFlag = "catalog-table-name" + catalogPartitioningFlag = "catalog-partitioning" + catalogPartitionByFlag = "catalog-partition-by" + catalogAuthTypeFlag = "catalog-auth-type" + dremioTokenEndpointFlag = "dremio-token-endpoint" //nolint:gosec // false positive + dremioPatFlag = "dremio-pat" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + + // Top-level fields + DisplayName *string + RunnerId *string + Description *string + Labels *map[string]string + + // Catalog fields + CatalogURI *string + CatalogWarehouse *string + CatalogNamespace *string + CatalogTableName *string + CatalogPartitioning *string + CatalogPartitionBy *[]string + + // Auth fields + CatalogAuthType *string + DremioTokenEndpoint *string + DremioToken *string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new Intake", + Long: "Creates a new Intake.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new Intake with required parameters`, + `$ stackit beta intake create --display-name my-intake --runner-id xxx --catalog-auth-type none --catalog-uri "http://dremio.example.com" --catalog-warehouse "my-warehouse"`), + examples.NewExample( + `Create a new Intake with a description, labels, and Dremio authentication`, + `$ stackit beta intake create --display-name my-intake --runner-id xxx --description "Production intake" --labels "env=prod,team=billing" --catalog-auth-type "dremio" --catalog-uri "http://dremio.example.com" --catalog-warehouse "my-warehouse" --dremio-token-endpoint "https://auth.dremio.cloud/oauth/token" --dremio-pat "MY_TOKEN"`), + examples.NewExample( + `Create a new Intake with manual partitioning by a date field`, + `$ stackit beta intake create --display-name my-partitioned-intake --runner-id xxx --catalog-auth-type "none" --catalog-uri "http://dremio.example.com" --catalog-warehouse "my-warehouse" --catalog-partitioning "manual" --catalog-partition-by "day(__intake_ts)"`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create an Intake for project %q?", projectLabel) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Intake: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Creating STACKIT Intake instance", func() error { + _, err = wait.CreateOrUpdateIntakeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resp.GetId()).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Instance creation: %w", err) + } + } + + return outputResult(p.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + // Top-level flags + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().Var(flags.UUIDFlag(), runnerIdFlag, "The UUID of the Intake Runner to use") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringToString(labelsFlag, nil, "Labels in key=value format, separated by commas. Example: --labels \"key1=value1,key2=value2\"") + + // Catalog flags + cmd.Flags().String(catalogURIFlag, "", "The URI to the Iceberg catalog endpoint") + cmd.Flags().String(catalogWarehouseFlag, "", "The Iceberg warehouse to connect to") + cmd.Flags().String(catalogNamespaceFlag, "", "The namespace to which data shall be written (default: 'intake')") + cmd.Flags().String(catalogTableNameFlag, "", "The table name to identify the table in Iceberg") + cmd.Flags().String(catalogPartitioningFlag, "", "The target table's partitioning. One of 'none', 'intake-time', 'manual'") + cmd.Flags().StringSlice(catalogPartitionByFlag, nil, "List of Iceberg partitioning expressions. Only used when --catalog-partitioning is 'manual'") + + // Auth flags + cmd.Flags().String(catalogAuthTypeFlag, "", "Authentication type for the catalog (e.g., 'none', 'dremio')") + cmd.Flags().String(dremioTokenEndpointFlag, "", "Dremio OAuth 2.0 token endpoint URL. Required if auth-type is 'dremio'") + cmd.Flags().String(dremioPatFlag, "", "Dremio personal access token. Required if auth-type is 'dremio'") + + err := flags.MarkFlagsRequired(cmd, displayNameFlag, runnerIdFlag, catalogURIFlag, catalogWarehouseFlag, catalogAuthTypeFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + // Top-level fields + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + RunnerId: flags.FlagToStringPointer(p, cmd, runnerIdFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + + // Catalog fields + CatalogURI: flags.FlagToStringPointer(p, cmd, catalogURIFlag), + CatalogWarehouse: flags.FlagToStringPointer(p, cmd, catalogWarehouseFlag), + CatalogNamespace: flags.FlagToStringPointer(p, cmd, catalogNamespaceFlag), + CatalogTableName: flags.FlagToStringPointer(p, cmd, catalogTableNameFlag), + CatalogPartitioning: flags.FlagToStringPointer(p, cmd, catalogPartitioningFlag), + CatalogPartitionBy: flags.FlagToStringSlicePointer(p, cmd, catalogPartitionByFlag), + + // Auth fields + CatalogAuthType: flags.FlagToStringPointer(p, cmd, catalogAuthTypeFlag), + DremioTokenEndpoint: flags.FlagToStringPointer(p, cmd, dremioTokenEndpointFlag), + DremioToken: flags.FlagToStringPointer(p, cmd, dremioPatFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiCreateIntakeRequest { + req := apiClient.DefaultAPI.CreateIntake(ctx, model.ProjectId, model.Region) + + // Build catalog authentication + var catalogAuth *intake.CatalogAuth + if model.CatalogAuthType != nil { + authType := intake.CatalogAuthType(*model.CatalogAuthType) + catalogAuth = &intake.CatalogAuth{ + Type: authType, + } + if *model.CatalogAuthType == "dremio" { + catalogAuth.Dremio = &intake.DremioAuth{ + TokenEndpoint: utils.PtrString(model.DremioTokenEndpoint), + PersonalAccessToken: utils.PtrString(model.DremioToken), + } + } + } + + var partitioning *intake.PartitioningType + if model.CatalogPartitioning != nil { + partitioning = utils.Ptr(intake.PartitioningType(*model.CatalogPartitioning)) + } + + // Build catalog + catalogPayload := intake.IntakeCatalog{ + Uri: utils.PtrString(model.CatalogURI), + Warehouse: utils.PtrString(model.CatalogWarehouse), + Namespace: model.CatalogNamespace, + TableName: model.CatalogTableName, + Partitioning: partitioning, + PartitionBy: utils.PtrValue(model.CatalogPartitionBy), + Auth: catalogAuth, + } + + // Build main payload + payload := intake.CreateIntakePayload{ + DisplayName: utils.PtrString(model.DisplayName), + IntakeRunnerId: utils.PtrString(model.RunnerId), + Description: model.Description, + Labels: utils.PtrValue(model.Labels), + Catalog: catalogPayload, + } + req = req.CreateIntakePayload(payload) + + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeResponse) error { + return p.OutputResult(model.OutputFormat, resp, func() error { + if resp == nil { + p.Outputf("Triggered creation of Intake for project %q, but no intake ID was returned.\n", projectLabel) + return nil + } + + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s Intake for project %q. Intake ID: %s\n", operationState, projectLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/intake/create/create_test.go b/internal/cmd/beta/intake/create/create_test.go new file mode 100644 index 000000000..a837e4d25 --- /dev/null +++ b/internal/cmd/beta/intake/create/create_test.go @@ -0,0 +1,365 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// Define a unique key for the context to avoid collisions +type testCtxKey struct{} + +const ( + testRegion = "eu01" + + testDisplayName = "testintake" + testDescription = "This is a test intake" + testLabelsString = "env=test,team=dev" + testCatalogURI = "http://dremio.example.com" + testCatalogWarehouse = "my-warehouse" + testCatalogNamespace = "test-namespace" + testCatalogTableName = "test-table" + testCatalogPartitioning = "manual" + testCatalogPartitionByFlag = "year,month" + testCatalogAuthType = "dremio" + testDremioTokenEndpoint = "https://auth.dremio.cloud/oauth/token" //nolint:gosec // false url + testDremioToken = "dremio-secret-token" +) + +var ( + // testCtx dummy context for testing purposes + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + // testClient mock API client + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testRunnerId = uuid.NewString() + + testLabels = map[string]string{"env": "test", "team": "dev"} + testCatalogPartitionBy = []string{"year", "month"} +) + +// fixtureFlagValues generates a map of flag values for tests +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + runnerIdFlag: testRunnerId, + descriptionFlag: testDescription, + labelsFlag: testLabelsString, + catalogURIFlag: testCatalogURI, + catalogWarehouseFlag: testCatalogWarehouse, + catalogNamespaceFlag: testCatalogNamespace, + catalogTableNameFlag: testCatalogTableName, + catalogPartitionByFlag: testCatalogPartitionByFlag, + catalogPartitioningFlag: testCatalogPartitioning, + catalogAuthTypeFlag: testCatalogAuthType, + dremioTokenEndpointFlag: testDremioTokenEndpoint, + dremioPatFlag: testDremioToken, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// fixtureInputModel generates an input model for tests +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: utils.Ptr(testDisplayName), + RunnerId: utils.Ptr(testRunnerId), + Description: utils.Ptr(testDescription), + Labels: utils.Ptr(testLabels), + CatalogURI: utils.Ptr(testCatalogURI), + CatalogWarehouse: utils.Ptr(testCatalogWarehouse), + CatalogNamespace: utils.Ptr(testCatalogNamespace), + CatalogTableName: utils.Ptr(testCatalogTableName), + CatalogPartitionBy: utils.Ptr(testCatalogPartitionBy), + CatalogPartitioning: utils.Ptr(testCatalogPartitioning), + CatalogAuthType: utils.Ptr(testCatalogAuthType), + DremioTokenEndpoint: utils.Ptr(testDremioTokenEndpoint), + DremioToken: utils.Ptr(testDremioToken), + } + for _, mod := range mods { + mod(model) + } + return model +} + +// fixtureCreatePayload generates a CreateIntakePayload for tests +func fixtureCreatePayload(mods ...func(payload *intake.CreateIntakePayload)) intake.CreateIntakePayload { + authType := intake.CatalogAuthType(testCatalogAuthType) + testPartitioningType := intake.PartitioningType(testCatalogPartitioning) + payload := intake.CreateIntakePayload{ + DisplayName: testDisplayName, + IntakeRunnerId: testRunnerId, + Description: utils.Ptr(testDescription), + Labels: testLabels, + Catalog: intake.IntakeCatalog{ + Uri: testCatalogURI, + Warehouse: testCatalogWarehouse, + Namespace: utils.Ptr(testCatalogNamespace), + TableName: utils.Ptr(testCatalogTableName), + Partitioning: &testPartitioningType, + PartitionBy: testCatalogPartitionBy, + Auth: &intake.CatalogAuth{ + Type: authType, + Dremio: &intake.DremioAuth{ + TokenEndpoint: testDremioTokenEndpoint, + PersonalAccessToken: testDremioToken, + }, + }, + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +// fixtureRequest generates an API request for tests +func fixtureRequest(mods ...func(request *intake.ApiCreateIntakeRequest)) intake.ApiCreateIntakeRequest { + request := testClient.DefaultAPI.CreateIntake(testCtx, testProjectId, testRegion) + request = request.CreateIntakePayload(fixtureCreatePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "runner-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, runnerIdFlag) + }), + isValid: false, + }, + { + description: "catalog-uri missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, catalogURIFlag) + }), + isValid: false, + }, + { + description: "catalog-warehouse missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, catalogWarehouseFlag) + }), + isValid: false, + }, + { + description: "required fields only", + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + runnerIdFlag: testRunnerId, + catalogURIFlag: testCatalogURI, + catalogWarehouseFlag: testCatalogWarehouse, + catalogAuthTypeFlag: testCatalogAuthType, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.Labels = nil + model.CatalogNamespace = nil + model.CatalogTableName = nil + model.CatalogPartitioning = nil + model.CatalogPartitionBy = nil + model.DremioTokenEndpoint = nil + model.DremioToken = nil + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiCreateIntakeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no optionals", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.Labels = nil + model.CatalogNamespace = nil + model.CatalogTableName = nil + model.CatalogPartitioning = nil + model.CatalogPartitionBy = nil + model.CatalogAuthType = nil + model.DremioTokenEndpoint = nil + model.DremioToken = nil + }), + expectedRequest: fixtureRequest(func(request *intake.ApiCreateIntakeRequest) { + *request = request.CreateIntakePayload(fixtureCreatePayload(func(payload *intake.CreateIntakePayload) { + payload.Description = nil + payload.Labels = nil + payload.Catalog.Namespace = nil + payload.Catalog.TableName = nil + payload.Catalog.PartitionBy = nil + payload.Catalog.Partitioning = nil + payload.Catalog.Auth = nil + })) + }), + }, + { + description: "auth type none", + model: fixtureInputModel(func(model *inputModel) { + model.CatalogAuthType = utils.Ptr("none") + model.DremioTokenEndpoint = nil + model.DremioToken = nil + }), + expectedRequest: fixtureRequest(func(request *intake.ApiCreateIntakeRequest) { + *request = request.CreateIntakePayload(fixtureCreatePayload(func(payload *intake.CreateIntakePayload) { + authType := intake.CatalogAuthType("none") + payload.Catalog.Auth.Type = authType + payload.Catalog.Auth.Dremio = nil + })) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + projectLabel string + resp *intake.IntakeResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{ + model: fixtureInputModel(), + projectLabel: "my-project", + resp: &intake.IntakeResponse{Id: "intake-id-123"}, + }, + wantErr: false, + }, + { + name: "default output - async", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Async = true + }), + projectLabel: "my-project", + resp: &intake.IntakeResponse{Id: "intake-id-123"}, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: &intake.IntakeResponse{Id: "intake-id-123"}, + }, + wantErr: false, + }, + { + name: "nil response - default output", + args: args{ + model: fixtureInputModel(), + resp: nil, + }, + wantErr: false, + }, + { + name: "nil response - json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: nil, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/delete/delete.go b/internal/cmd/beta/intake/delete/delete.go new file mode 100644 index 000000000..e831e2493 --- /dev/null +++ b/internal/cmd/beta/intake/delete/delete.go @@ -0,0 +1,115 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + intakeIdArg = "INTAKE_ID" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + IntakeId string +} + +// NewCmd creates a new cobra command for deleting an Intake +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", intakeIdArg), + Short: "Deletes an Intake", + Long: "Deletes an Intake.", + Args: args.SingleArg(intakeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete an Intake with ID "xxx"`, + `$ stackit beta intake delete xxx`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + prompt := fmt.Sprintf("Are you sure you want to delete an Intake %q?", model.IntakeId) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + if err = req.Execute(); err != nil { + return fmt.Errorf("delete Intake: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Deleting STACKIT Intake instance", func() error { + _, err = wait.DeleteIntakeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.IntakeId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Instance deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + p.Printer.Outputf("%s STACKIT Intake instance %s \n", operationState, model.IntakeId) + + return nil + }, + } + return cmd +} + +// parseInput parses the command arguments and flags into a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + intakeId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: intakeId, + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to delete an Intake +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiDeleteIntakeRequest { + req := apiClient.DefaultAPI.DeleteIntake(ctx, model.ProjectId, model.Region, model.IntakeId) + return req +} diff --git a/internal/cmd/beta/intake/delete/delete_test.go b/internal/cmd/beta/intake/delete/delete_test.go new file mode 100644 index 000000000..31e631fd3 --- /dev/null +++ b/internal/cmd/beta/intake/delete/delete_test.go @@ -0,0 +1,159 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +// Define a unique key for the context to avoid collisions +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + // testCtx is a dummy context for testing purposes + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + // testClient is a mock API client + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() +) + +// fixtureArgValues generates a slice of arguments for tests +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testIntakeId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// fixtureFlagValues generates a map of flag values for tests +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// fixtureInputModel generates an input model for tests +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: testIntakeId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// fixtureRequest generates an API request for tests +func fixtureRequest(mods ...func(request *intake.ApiDeleteIntakeRequest)) intake.ApiDeleteIntakeRequest { + request := testClient.DefaultAPI.DeleteIntake(testCtx, testProjectId, testRegion, testIntakeId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake id invalid", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiDeleteIntakeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/intake/describe/describe.go b/internal/cmd/beta/intake/describe/describe.go new file mode 100644 index 000000000..91dbc0771 --- /dev/null +++ b/internal/cmd/beta/intake/describe/describe.go @@ -0,0 +1,145 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + intakeIdArg = "INTAKE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + IntakeId string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", intakeIdArg), + Short: "Shows details of an Intake", + Long: "Shows details of an Intake.", + Args: args.SingleArg(intakeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of an Intake with ID "xxx"`, + `$ stackit beta intake describe xxx`), + examples.NewExample( + `Get details of an Intake with ID "xxx" in JSON format`, + `$ stackit beta intake describe xxx --output-format json`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get Intake: %w", err) + } + + return outputResult(p.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + intakeId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: intakeId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiGetIntakeRequest { + req := apiClient.DefaultAPI.GetIntake(ctx, model.ProjectId, model.Region, model.IntakeId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, intk *intake.IntakeResponse) error { + if intk == nil { + return fmt.Errorf("received nil response, could not display details") + } + + return p.OutputResult(outputFormat, intk, func() error { + table := tables.NewTable() + table.SetHeader("Attribute", "Value") + + table.AddRow("ID", intk.GetId()) + table.AddRow("Name", intk.GetDisplayName()) + table.AddRow("State", intk.GetState()) + table.AddRow("Runner ID", intk.GetIntakeRunnerId()) + table.AddRow("Created", intk.GetCreateTime()) + table.AddRow("Labels", intk.GetLabels()) + + if description := intk.GetDescription(); description != "" { + table.AddRow("Description", description) + } + + if failureMessage := intk.GetFailureMessage(); failureMessage != "" { + table.AddRow("Failure Message", failureMessage) + } + + table.AddSeparator() + table.AddRow("Ingestion URI", intk.GetUri()) + table.AddRow("Topic", intk.GetTopic()) + table.AddRow("Dead Letter Topic", intk.GetDeadLetterTopic()) + table.AddRow("Undelivered Messages", intk.GetUndeliveredMessageCount()) + + table.AddSeparator() + catalog := intk.GetCatalog() + table.AddRow("Catalog URI", catalog.GetUri()) + table.AddRow("Catalog Warehouse", catalog.GetWarehouse()) + if namespace := catalog.GetNamespace(); namespace != "" { + table.AddRow("Catalog Namespace", namespace) + } + if tableName := catalog.GetTableName(); tableName != "" { + table.AddRow("Catalog Table Name", tableName) + } + table.AddRow("Catalog Partitioning", catalog.GetPartitioning()) + if partitionBy := catalog.GetPartitionBy(); len(partitionBy) > 0 { + table.AddRow("Catalog Partition By", strings.Join(partitionBy, ", ")) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/beta/intake/describe/describe_test.go b/internal/cmd/beta/intake/describe/describe_test.go new file mode 100644 index 000000000..5d1fad69e --- /dev/null +++ b/internal/cmd/beta/intake/describe/describe_test.go @@ -0,0 +1,197 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testIntakeId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: testIntakeId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiGetIntakeRequest)) intake.ApiGetIntakeRequest { + request := testClient.DefaultAPI.GetIntake(testCtx, testProjectId, testRegion, testIntakeId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake id invalid", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiGetIntakeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + intakeResp *intake.IntakeResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", intakeResp: &intake.IntakeResponse{Catalog: intake.IntakeCatalog{}}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, intakeResp: &intake.IntakeResponse{Catalog: intake.IntakeCatalog{}}}, + wantErr: false, + }, + { + name: "yaml output", + args: args{outputFormat: print.YAMLOutputFormat, intakeResp: &intake.IntakeResponse{Catalog: intake.IntakeCatalog{}}}, + wantErr: false, + }, + { + name: "nil response", + args: args{intakeResp: nil}, + wantErr: true, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.intakeResp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/intake.go b/internal/cmd/beta/intake/intake.go new file mode 100644 index 000000000..8e90bddd6 --- /dev/null +++ b/internal/cmd/beta/intake/intake.go @@ -0,0 +1,41 @@ +package intake + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/update" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/user" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// NewCmd creates the 'stackit intake' command +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "intake", + Short: "Provides functionality for intake", + Long: "Provides functionality for intake.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(runner.NewCmd(params)) + cmd.AddCommand(user.NewCmd(params)) + + // Intake instance subcommands + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) +} diff --git a/internal/cmd/beta/intake/list/list.go b/internal/cmd/beta/intake/list/list.go new file mode 100644 index 000000000..7fa509260 --- /dev/null +++ b/internal/cmd/beta/intake/list/list.go @@ -0,0 +1,151 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const ( + limitFlag = "limit" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +// NewCmd creates a new cobra command for listing Intakes +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all Intakes", + Long: "Lists all Intakes for the current project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all Intakes`, + `$ stackit beta intake list`), + examples.NewExample( + `List all Intakes in JSON format`, + `$ stackit beta intake list --output-format json`), + examples.NewExample( + `List up to 5 Intakes`, + `$ stackit beta intake list --limit 5`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list Intakes: %w", err) + } + intakes := resp.GetIntakes() + + // Truncate output + if model.Limit != nil && len(intakes) > int(*model.Limit) { + intakes = intakes[:*model.Limit] + } + + projectLabel := model.ProjectId + if len(intakes) == 0 { + projectLabel, err = projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + } + } + + return outputResult(p.Printer, model.OutputFormat, projectLabel, intakes) + }, + } + configureFlags(cmd) + return cmd +} + +// configureFlags adds the --limit flag to the command +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +// parseInput parses the command flags into a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to list Intakes +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiListIntakesRequest { + req := apiClient.DefaultAPI.ListIntakes(ctx, model.ProjectId, model.Region) + return req +} + +// outputResult formats the API response and prints it to the console +func outputResult(p *print.Printer, outputFormat, projectLabel string, intakes []intake.IntakeResponse) error { + return p.OutputResult(outputFormat, intakes, func() error { + if len(intakes) == 0 { + p.Outputf("No intakes found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "STATE", "RUNNER ID") + for i := range intakes { + intakeItem := intakes[i] + table.AddRow( + intakeItem.GetId(), + intakeItem.GetDisplayName(), + intakeItem.GetState(), + intakeItem.GetIntakeRunnerId(), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/intake/list/list_test.go b/internal/cmd/beta/intake/list/list_test.go new file mode 100644 index 000000000..9a46f2a7b --- /dev/null +++ b/internal/cmd/beta/intake/list/list_test.go @@ -0,0 +1,209 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testLimit = int64(5) +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiListIntakesRequest)) intake.ApiListIntakesRequest { + request := testClient.DefaultAPI.ListIntakes(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = strconv.FormatInt(testLimit, 10) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(testLimit) + }), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "limit is zero", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "limit is negative", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiListIntakesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + intakes []intake.IntakeResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", intakes: []intake.IntakeResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, intakes: []intake.IntakeResponse{}}, + wantErr: false, + }, + { + name: "empty slice", + args: args{intakes: []intake.IntakeResponse{}}, + wantErr: false, + }, + { + name: "nil slice", + args: args{intakes: nil}, + wantErr: false, + }, + { + name: "empty intake in slice", + args: args{ + intakes: []intake.IntakeResponse{{}}, + }, + wantErr: false, + }, + { + name: "with project label", + args: args{ + projectLabel: "my-project", + intakes: []intake.IntakeResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.intakes); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/runner/create/create.go b/internal/cmd/beta/intake/runner/create/create.go new file mode 100644 index 000000000..c811ec896 --- /dev/null +++ b/internal/cmd/beta/intake/runner/create/create.go @@ -0,0 +1,169 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + displayNameFlag = "display-name" + maxMessageSizeKiBFlag = "max-message-size-kib" + maxMessagesPerHourFlag = "max-messages-per-hour" + descriptionFlag = "description" + labelFlag = "labels" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + DisplayName *string + MaxMessageSizeKiB *int32 + MaxMessagesPerHour *int32 + Description *string + Labels *map[string]string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new Intake Runner", + Long: "Creates a new Intake Runner.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new Intake Runner with a display name and message capacity limits`, + `$ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000`), + examples.NewExample( + `Create a new Intake Runner with a description and labels`, + `$ stackit beta intake runner create --display-name my-runner --max-message-size-kib 1000 --max-messages-per-hour 5000 --description "Main runner for production" --labels="env=prod,team=billing"`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create an Intake Runner for project %q?", projectLabel) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Intake Runner: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Creating STACKIT Intake Runner", func() error { + _, err = wait.CreateOrUpdateIntakeRunnerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resp.GetId()).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Intake Runner creation: %w", err) + } + } + + return outputResult(p.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().Int32(maxMessageSizeKiBFlag, 0, "Maximum message size in KiB") + cmd.Flags().Int32(maxMessagesPerHourFlag, 0, "Maximum number of messages per hour") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringToString(labelFlag, nil, "Labels in key=value format, separated by commas. Example: --labels \"key1=value1,key2=value2\"") + + err := flags.MarkFlagsRequired(cmd, displayNameFlag, maxMessageSizeKiBFlag, maxMessagesPerHourFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + MaxMessageSizeKiB: flags.FlagToInt32Pointer(p, cmd, maxMessageSizeKiBFlag), + MaxMessagesPerHour: flags.FlagToInt32Pointer(p, cmd, maxMessagesPerHourFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiCreateIntakeRunnerRequest { + // Start building the request by calling the base method with path parameters + req := apiClient.DefaultAPI.CreateIntakeRunner(ctx, model.ProjectId, model.Region) + + // Create the payload struct with data from the input model + payload := intake.CreateIntakeRunnerPayload{ + DisplayName: utils.PtrString(model.DisplayName), + MaxMessageSizeKiB: utils.PtrValue(model.MaxMessageSizeKiB), + MaxMessagesPerHour: utils.PtrValue(model.MaxMessagesPerHour), + Description: model.Description, + Labels: utils.PtrValue(model.Labels), + } + // Attach the payload to the request builder + req = req.CreateIntakeRunnerPayload(payload) + + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeRunnerResponse) error { + return p.OutputResult(model.OutputFormat, resp, func() error { + if resp == nil { + p.Outputf("Triggered creation of Intake Runner for project %q, but no runner ID was returned.\n", projectLabel) + return nil + } + + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s Intake Runner for project %q. Runner ID: %s\n", operationState, projectLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/intake/runner/create/create_test.go b/internal/cmd/beta/intake/runner/create/create_test.go new file mode 100644 index 000000000..eb942d8bd --- /dev/null +++ b/internal/cmd/beta/intake/runner/create/create_test.go @@ -0,0 +1,297 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// Define a unique key for the context to avoid collisions +type testCtxKey struct{} + +const ( + testRegion = "eu01" + testDisplayName = "testrunner" + testMaxMessageSizeKiB = int32(1024) + testMaxMessagesPerHour = int32(10000) + testDescription = "This is a test runner" + testLabelsString = "env=test,team=dev" +) + +var ( + // testCtx dummy context for testing purposes + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + // testClient mock API client + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + + testLabels = map[string]string{"env": "test", "team": "dev"} +) + +// fixtureFlagValues generates a map of flag values for tests +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + maxMessageSizeKiBFlag: "1024", + maxMessagesPerHourFlag: "10000", + descriptionFlag: testDescription, + labelFlag: testLabelsString, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// fixtureInputModel generates an input model for tests +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: utils.Ptr(testDisplayName), + MaxMessageSizeKiB: utils.Ptr(testMaxMessageSizeKiB), + MaxMessagesPerHour: utils.Ptr(testMaxMessagesPerHour), + Description: utils.Ptr(testDescription), + Labels: utils.Ptr(testLabels), + } + for _, mod := range mods { + mod(model) + } + return model +} + +// fixtureCreatePayload generates a CreateIntakeRunnerPayload for tests +func fixtureCreatePayload(mods ...func(payload *intake.CreateIntakeRunnerPayload)) intake.CreateIntakeRunnerPayload { + payload := intake.CreateIntakeRunnerPayload{ + DisplayName: testDisplayName, + MaxMessageSizeKiB: testMaxMessageSizeKiB, + MaxMessagesPerHour: testMaxMessagesPerHour, + Description: utils.Ptr(testDescription), + Labels: testLabels, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +// fixtureRequest generates an API request for tests +func fixtureRequest(mods ...func(request *intake.ApiCreateIntakeRunnerRequest)) intake.ApiCreateIntakeRunnerRequest { + request := testClient.DefaultAPI.CreateIntakeRunner(testCtx, testProjectId, testRegion) + request = request.CreateIntakeRunnerPayload(fixtureCreatePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "display name missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + { + description: "max message size missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, maxMessageSizeKiBFlag) + }), + isValid: false, + }, + { + description: "max messages per hour missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, maxMessagesPerHourFlag) + }), + isValid: false, + }, + { + description: "required fields only", + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + maxMessageSizeKiBFlag: "1024", + maxMessagesPerHourFlag: "10000", + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.Labels = nil + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + parseInputWrapper := func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + } + testutils.TestParseInput(t, NewCmd, parseInputWrapper, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiCreateIntakeRunnerRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no optionals", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.Labels = nil + }), + expectedRequest: fixtureRequest(func(request *intake.ApiCreateIntakeRunnerRequest) { + *request = request.CreateIntakeRunnerPayload(fixtureCreatePayload(func(payload *intake.CreateIntakeRunnerPayload) { + payload.Description = nil + payload.Labels = nil + })) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + projectLabel string + resp *intake.IntakeRunnerResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{ + model: fixtureInputModel(), + projectLabel: "my-project", + resp: &intake.IntakeRunnerResponse{Id: "runner-id-123"}, + }, + wantErr: false, + }, + { + name: "default output - async", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Async = true + }), + projectLabel: "my-project", + resp: &intake.IntakeRunnerResponse{Id: "runner-id-123"}, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: &intake.IntakeRunnerResponse{Id: "runner-id-123"}, + }, + wantErr: false, + }, + { + name: "nil response - default output", + args: args{ + model: fixtureInputModel(), + resp: nil, + }, + wantErr: false, + }, + { + name: "nil response - json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: nil, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/runner/delete/delete.go b/internal/cmd/beta/intake/runner/delete/delete.go new file mode 100644 index 000000000..fe54853d5 --- /dev/null +++ b/internal/cmd/beta/intake/runner/delete/delete.go @@ -0,0 +1,133 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + runnerIdArg = "RUNNER_ID" + forceDeleteFlag = "force" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + RunnerId string + ForceDelete bool +} + +// NewCmd creates a new cobra command for deleting an Intake Runner +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", runnerIdArg), + Short: "Deletes an Intake Runner", + Long: "Deletes an Intake Runner.", + Args: args.SingleArg(runnerIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete an Intake Runner with ID "xxx"`, + `$ stackit beta intake runner delete xxx`), + examples.NewExample( + `Delete an Intake Runner with ID "xxx", along with all associated Intakes and Intake Users that would stop the removal of the Intake`, + `$ stackit beta intake runner delete xxx --force`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + prompt := fmt.Sprintf("Are you sure you want to delete Intake Runner %q?", model.RunnerId) + if model.ForceDelete { + prompt = fmt.Sprintf("%s This will also remove all Intakes and Intake Users that would stop the removal of the Intake", prompt) + } + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + if err = req.Execute(); err != nil { + return fmt.Errorf("delete Intake Runner: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Deleting STACKIT Intake Runner", func() error { + _, err = wait.DeleteIntakeRunnerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.RunnerId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Intake Runner deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + p.Printer.Outputf("%s stackit Intake Runner %s \n", operationState, model.RunnerId) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(forceDeleteFlag, false, "When true, also removes all associated Intakes and Intake Users that would stop the removal of the Intake") +} + +// parseInput parses the command arguments and flags into a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + runnerId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + RunnerId: runnerId, + ForceDelete: flags.FlagToBoolValue(p, cmd, forceDeleteFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to delete an Intake Runner +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiDeleteIntakeRunnerRequest { + req := apiClient.DefaultAPI.DeleteIntakeRunner(ctx, model.ProjectId, model.Region, model.RunnerId) + if model.ForceDelete { + return req.Force(true) + } + return req +} diff --git a/internal/cmd/beta/intake/runner/delete/delete_test.go b/internal/cmd/beta/intake/runner/delete/delete_test.go new file mode 100644 index 000000000..2bc4709ca --- /dev/null +++ b/internal/cmd/beta/intake/runner/delete/delete_test.go @@ -0,0 +1,177 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +// Define a unique key for the context to avoid collisions +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + // testCtx is a dummy context for testing purposes + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + // testClient is a mock API client + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testRunnerId = uuid.NewString() +) + +// fixtureArgValues generates a slice of arguments for tests +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRunnerId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// fixtureFlagValues generates a map of flag values for tests +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// fixtureInputModel generates an input model for tests +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + RunnerId: testRunnerId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// fixtureRequest generates an API request for tests +func fixtureRequest(mods ...func(request *intake.ApiDeleteIntakeRunnerRequest)) intake.ApiDeleteIntakeRunnerRequest { + request := testClient.DefaultAPI.DeleteIntakeRunner(testCtx, testProjectId, testRegion, testRunnerId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with force", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[forceDeleteFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ForceDelete = true + }), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "runner id invalid", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiDeleteIntakeRunnerRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "with force", + model: fixtureInputModel(func(model *inputModel) { + model.ForceDelete = true + }), + expectedRequest: fixtureRequest().Force(true), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/intake/runner/describe/describe.go b/internal/cmd/beta/intake/runner/describe/describe.go new file mode 100644 index 000000000..6eafda0fd --- /dev/null +++ b/internal/cmd/beta/intake/runner/describe/describe.go @@ -0,0 +1,118 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + runnerIdArg = "RUNNER_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + RunnerId string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", runnerIdArg), + Short: "Shows details of an Intake Runner", + Long: "Shows details of an Intake Runner.", + Args: args.SingleArg(runnerIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of an Intake Runner with ID "xxx"`, + `$ stackit beta intake runner describe xxx`), + examples.NewExample( + `Get details of an Intake Runner with ID "xxx" in JSON format`, + `$ stackit beta intake runner describe xxx --output-format json`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API to get a single runner + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get Intake Runner: %w", err) + } + + return outputResult(p.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + runnerId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + RunnerId: runnerId, + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to get a single Intake Runner +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiGetIntakeRunnerRequest { + req := apiClient.DefaultAPI.GetIntakeRunner(ctx, model.ProjectId, model.Region, model.RunnerId) + return req +} + +// outputResult formats the API response and prints it to the console +func outputResult(p *print.Printer, outputFormat string, runner *intake.IntakeRunnerResponse) error { + if runner == nil { + return fmt.Errorf("received nil runner, could not display details") + } + return p.OutputResult(outputFormat, runner, func() error { + table := tables.NewTable() + table.SetHeader("Attribute", "Value") + table.AddRow("ID", runner.GetId()) + table.AddRow("Name", runner.GetDisplayName()) + table.AddRow("State", runner.GetState()) + table.AddRow("Created", runner.GetCreateTime()) + table.AddRow("Labels", runner.GetLabels()) + table.AddRow("Description", runner.GetDescription()) + table.AddRow("Max Message Size (KiB)", runner.GetMaxMessageSizeKiB()) + table.AddRow("Max Messages/Hour", runner.GetMaxMessagesPerHour()) + table.AddRow("Ingestion URI", runner.GetUri()) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/intake/runner/describe/describe_test.go b/internal/cmd/beta/intake/runner/describe/describe_test.go new file mode 100644 index 000000000..e4911fa1e --- /dev/null +++ b/internal/cmd/beta/intake/runner/describe/describe_test.go @@ -0,0 +1,196 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testRunnerId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRunnerId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + RunnerId: testRunnerId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiGetIntakeRunnerRequest)) intake.ApiGetIntakeRunnerRequest { + request := testClient.DefaultAPI.GetIntakeRunner(testCtx, testProjectId, testRegion, testRunnerId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "runner id invalid", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiGetIntakeRunnerRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + runner *intake.IntakeRunnerResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", runner: &intake.IntakeRunnerResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, runner: &intake.IntakeRunnerResponse{}}, + wantErr: false, + }, + { + name: "yaml output", + args: args{outputFormat: print.YAMLOutputFormat, runner: &intake.IntakeRunnerResponse{}}, + wantErr: false, + }, + { + name: "nil runner", + args: args{runner: nil}, + wantErr: true, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.runner); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/runner/list/list.go b/internal/cmd/beta/intake/runner/list/list.go new file mode 100644 index 000000000..63d278516 --- /dev/null +++ b/internal/cmd/beta/intake/runner/list/list.go @@ -0,0 +1,154 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + limitFlag = "limit" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +// NewCmd creates a new cobra command for listing Intake Runners +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all Intake Runners", + Long: "Lists all Intake Runners for the current project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all Intake Runners`, + `$ stackit beta intake runner list`), + examples.NewExample( + `List all Intake Runners in JSON format`, + `$ stackit beta intake runner list --output-format json`), + examples.NewExample( + `List up to 5 Intake Runners`, + `$ stackit beta intake runner list --limit 5`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list Intake Runners: %w", err) + } + runners := resp.GetIntakeRunners() + + // Truncate output + if model.Limit != nil && len(runners) > int(*model.Limit) { + runners = runners[:*model.Limit] + } + + projectLabel := model.ProjectId + if len(runners) == 0 { + projectLabel, err = projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + } + } + + return outputResult(p.Printer, model.OutputFormat, projectLabel, runners) + }, + } + configureFlags(cmd) + return cmd +} + +// configureFlags adds the --limit flag to the command +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +// parseInput parses the command flags into a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to list Intake Runners +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiListIntakeRunnersRequest { + req := apiClient.DefaultAPI.ListIntakeRunners(ctx, model.ProjectId, model.Region) + // Note: we do support API pagination, but for consistency with other services, we fetch all items and apply + // client-side limit. + // A more advanced implementation could use the --limit flag to set the API's PageSize. + return req +} + +// outputResult formats the API response and prints it to the console +func outputResult(p *print.Printer, outputFormat, projectLabel string, runners []intake.IntakeRunnerResponse) error { + return p.OutputResult(outputFormat, runners, func() error { + if len(runners) == 0 { + p.Outputf("No intake runners found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + + table.SetHeader("ID", "NAME", "STATE") + for _, runner := range runners { + table.AddRow( + runner.GetId(), + runner.GetDisplayName(), + runner.GetState(), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/intake/runner/list/list_test.go b/internal/cmd/beta/intake/runner/list/list_test.go new file mode 100644 index 000000000..edddbc942 --- /dev/null +++ b/internal/cmd/beta/intake/runner/list/list_test.go @@ -0,0 +1,201 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" + testLimit = int64(5) +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiListIntakeRunnersRequest)) intake.ApiListIntakeRunnersRequest { + request := testClient.DefaultAPI.ListIntakeRunners(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = strconv.FormatInt(testLimit, 10) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(testLimit) + }), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "limit is zero", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "limit is negative", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiListIntakeRunnersRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + runners []intake.IntakeRunnerResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", runners: []intake.IntakeRunnerResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, runners: []intake.IntakeRunnerResponse{}}, + wantErr: false, + }, + { + name: "empty slice", + args: args{runners: []intake.IntakeRunnerResponse{}}, + wantErr: false, + }, + { + name: "nil slice", + args: args{runners: nil}, + wantErr: false, + }, + { + name: "empty intake runner in slice", + args: args{ + runners: []intake.IntakeRunnerResponse{{}}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, "dummy-projectlabel", tt.args.runners); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/runner/runner.go b/internal/cmd/beta/intake/runner/runner.go new file mode 100644 index 000000000..4c6d58607 --- /dev/null +++ b/internal/cmd/beta/intake/runner/runner.go @@ -0,0 +1,36 @@ +package runner + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/runner/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "runner", + Short: "Provides functionality for Intake Runners", + Long: "Provides functionality for Intake Runners.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + // Pass the params down to each action command + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/beta/intake/runner/update/update.go b/internal/cmd/beta/intake/runner/update/update.go new file mode 100644 index 000000000..36b5b088e --- /dev/null +++ b/internal/cmd/beta/intake/runner/update/update.go @@ -0,0 +1,178 @@ +package update + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + runnerIdArg = "RUNNER_ID" +) + +const ( + displayNameFlag = "display-name" + maxMessageSizeKiBFlag = "max-message-size-kib" + maxMessagesPerHourFlag = "max-messages-per-hour" + descriptionFlag = "description" + labelFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + RunnerId string + DisplayName *string + MaxMessageSizeKiB *int32 + MaxMessagesPerHour *int32 + Description *string + Labels *map[string]string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", runnerIdArg), + Short: "Updates an Intake Runner", + Long: "Updates an Intake Runner. Only the specified fields are updated.", + Args: args.SingleArg(runnerIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the display name of an Intake Runner with ID "xxx"`, + `$ stackit beta intake runner update xxx --display-name "new-runner-name"`), + examples.NewExample( + `Update the message capacity limits for an Intake Runner with ID "xxx"`, + `$ stackit beta intake runner update xxx --max-message-size-kib 1000 --max-messages-per-hour 10000`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update Intake Runner: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Updating STACKIT Intake Runner", func() error { + _, err = wait.CreateOrUpdateIntakeRunnerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.RunnerId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Intake Runner update: %w", err) + } + } + + return outputResult(p.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().Int32(maxMessageSizeKiBFlag, 0, "Maximum message size in KiB. Note: Overall message capacity cannot be decreased.") + cmd.Flags().Int32(maxMessagesPerHourFlag, 0, "Maximum number of messages per hour. Note: Overall message capacity cannot be decreased.") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringToString(labelFlag, nil, `Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2".`) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + runnerId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + RunnerId: runnerId, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + MaxMessageSizeKiB: flags.FlagToInt32Pointer(p, cmd, maxMessageSizeKiBFlag), + MaxMessagesPerHour: flags.FlagToInt32Pointer(p, cmd, maxMessagesPerHourFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + } + + if model.DisplayName == nil && model.MaxMessageSizeKiB == nil && model.MaxMessagesPerHour == nil && model.Description == nil && model.Labels == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiUpdateIntakeRunnerRequest { + req := apiClient.DefaultAPI.UpdateIntakeRunner(ctx, model.ProjectId, model.Region, model.RunnerId) + + payload := intake.UpdateIntakeRunnerPayload{} + if model.DisplayName != nil { + payload.DisplayName = model.DisplayName + } + if model.MaxMessageSizeKiB != nil { + payload.MaxMessageSizeKiB = model.MaxMessageSizeKiB + } + if model.MaxMessagesPerHour != nil { + payload.MaxMessagesPerHour = model.MaxMessagesPerHour + } + if model.Description != nil { + payload.Description = model.Description + } + if model.Labels != nil { + payload.Labels = utils.PtrValue(model.Labels) + } + + req = req.UpdateIntakeRunnerPayload(payload) + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeRunnerResponse) error { + return p.OutputResult(model.OutputFormat, resp, func() error { + if resp == nil { + p.Outputf("Triggered update of Intake Runner for project %q, but no runner ID was returned.\n", projectLabel) + return nil + } + + operationState := "Updated" + if model.Async { + operationState = "Triggered update of" + } + p.Outputf("%s Intake Runner for project %q. Runner ID: %s\n", operationState, projectLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/intake/runner/update/update_test.go b/internal/cmd/beta/intake/runner/update/update_test.go new file mode 100644 index 000000000..22696d724 --- /dev/null +++ b/internal/cmd/beta/intake/runner/update/update_test.go @@ -0,0 +1,281 @@ +package update + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testRunnerId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRunnerId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: "new-runner-name", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + RunnerId: testRunnerId, + DisplayName: utils.Ptr("new-runner-name"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiUpdateIntakeRunnerRequest)) intake.ApiUpdateIntakeRunnerRequest { + request := testClient.DefaultAPI.UpdateIntakeRunner(testCtx, testProjectId, testRegion, testRunnerId) + payload := intake.UpdateIntakeRunnerPayload{ + DisplayName: utils.Ptr("new-runner-name"), + } + request = request.UpdateIntakeRunnerPayload(payload) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no update flags provided", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + }, + isValid: false, + }, + { + description: "update all fields", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[maxMessageSizeKiBFlag] = "2048" + flagValues[maxMessagesPerHourFlag] = "10000" + flagValues[descriptionFlag] = "new description" + flagValues[labelFlag] = "env=prod,team=sre" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.MaxMessageSizeKiB = utils.Ptr(int32(2048)) + model.MaxMessagesPerHour = utils.Ptr(int32(10000)) + model.Description = utils.Ptr("new description") + model.Labels = utils.Ptr(map[string]string{"env": "prod", "team": "sre"}) + }), + }, + { + description: "no args", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiUpdateIntakeRunnerRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "update description and labels", + model: fixtureInputModel(func(model *inputModel) { + model.DisplayName = nil + model.Description = utils.Ptr("new-desc") + model.Labels = utils.Ptr(map[string]string{"key": "value"}) + }), + expectedRequest: fixtureRequest(func(request *intake.ApiUpdateIntakeRunnerRequest) { + payload := intake.UpdateIntakeRunnerPayload{ + Description: utils.Ptr("new-desc"), + Labels: map[string]string{"key": "value"}, + } + *request = request.UpdateIntakeRunnerPayload(payload) + }), + }, + { + description: "update all fields", + model: fixtureInputModel(func(model *inputModel) { + model.DisplayName = utils.Ptr("another-name") + model.MaxMessageSizeKiB = utils.Ptr(int32(4096)) + model.MaxMessagesPerHour = utils.Ptr(int32(20000)) + model.Description = utils.Ptr("final-desc") + model.Labels = utils.Ptr(map[string]string{"a": "b"}) + }), + expectedRequest: fixtureRequest(func(request *intake.ApiUpdateIntakeRunnerRequest) { + payload := intake.UpdateIntakeRunnerPayload{ + DisplayName: utils.Ptr("another-name"), + MaxMessageSizeKiB: utils.Ptr(int32(4096)), + MaxMessagesPerHour: utils.Ptr(int32(20000)), + Description: utils.Ptr("final-desc"), + Labels: map[string]string{"a": "b"}, + } + *request = request.UpdateIntakeRunnerPayload(payload) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + projectLabel string + resp *intake.IntakeRunnerResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{ + model: fixtureInputModel(), + projectLabel: "my-project", + resp: &intake.IntakeRunnerResponse{Id: "runner-id-123"}, + }, + wantErr: false, + }, + { + name: "default output - async", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Async = true + }), + projectLabel: "my-project", + resp: &intake.IntakeRunnerResponse{Id: "runner-id-123"}, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: &intake.IntakeRunnerResponse{Id: "runner-id-123"}, + }, + wantErr: false, + }, + { + name: "nil response - default output", + args: args{ + model: fixtureInputModel(), + resp: nil, + }, + wantErr: false, + }, + { + name: "nil response - json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: nil, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/update/update.go b/internal/cmd/beta/intake/update/update.go new file mode 100644 index 000000000..fb6522c7d --- /dev/null +++ b/internal/cmd/beta/intake/update/update.go @@ -0,0 +1,267 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + intakeIdArg = "INTAKE_ID" + + // Top-level flags + displayNameFlag = "display-name" + runnerIdFlag = "runner-id" + descriptionFlag = "description" + labelsFlag = "labels" + + // Catalog flags + catalogURIFlag = "catalog-uri" + catalogWarehouseFlag = "catalog-warehouse" + catalogNamespaceFlag = "catalog-namespace" + catalogTableNameFlag = "catalog-table-name" + + // Auth flags + catalogAuthTypeFlag = "catalog-auth-type" + dremioTokenEndpointFlag = "dremio-token-endpoint" //nolint:gosec // false positive + dremioPatFlag = "dremio-pat" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + // Main + IntakeId string + DisplayName *string + RunnerId *string + Description *string + Labels *map[string]string + + // Catalog + CatalogURI *string + CatalogWarehouse *string + CatalogNamespace *string + CatalogTableName *string + + // Auth + CatalogAuthType *string + DremioTokenEndpoint *string + DremioToken *string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", intakeIdArg), + Short: "Updates an Intake", + Long: "Updates an Intake. Only the specified fields are updated.", + Args: args.SingleArg(intakeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the display name of an Intake with ID "xxx"`, + `$ stackit beta intake update xxx --runner-id yyy --display-name new-intake-name`), + examples.NewExample( + `Update the catalog details for an Intake with ID "xxx"`, + `$ stackit beta intake update xxx --runner-id yyy --catalog-uri "http://new.uri" --catalog-warehouse "new-warehouse"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update Intake: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Updating STACKIT Intake Runner instance", func() error { + _, err = wait.CreateOrUpdateIntakeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.IntakeId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Instance creation: %w", err) + } + } + + return outputResult(p.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + // Top-level flags + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().Var(flags.UUIDFlag(), runnerIdFlag, "The UUID of the Intake Runner to use") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringToString(labelsFlag, nil, `Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2".`) + + // Catalog flags + cmd.Flags().String(catalogURIFlag, "", "The URI to the Iceberg catalog endpoint") + cmd.Flags().String(catalogWarehouseFlag, "", "The Iceberg warehouse to connect to") + cmd.Flags().String(catalogNamespaceFlag, "", "The namespace to which data shall be written") + cmd.Flags().String(catalogTableNameFlag, "", "The table name to identify the table in Iceberg") + + // Auth flags + cmd.Flags().String(catalogAuthTypeFlag, "", "Authentication type for the catalog (e.g., 'none', 'dremio')") + cmd.Flags().String(dremioTokenEndpointFlag, "", "Dremio OAuth 2.0 token endpoint URL") + cmd.Flags().String(dremioPatFlag, "", "Dremio personal access token") + + err := flags.MarkFlagsRequired(cmd, runnerIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + intakeId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: intakeId, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + RunnerId: flags.FlagToStringPointer(p, cmd, runnerIdFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + CatalogURI: flags.FlagToStringPointer(p, cmd, catalogURIFlag), + CatalogWarehouse: flags.FlagToStringPointer(p, cmd, catalogWarehouseFlag), + CatalogNamespace: flags.FlagToStringPointer(p, cmd, catalogNamespaceFlag), + CatalogTableName: flags.FlagToStringPointer(p, cmd, catalogTableNameFlag), + CatalogAuthType: flags.FlagToStringPointer(p, cmd, catalogAuthTypeFlag), + DremioTokenEndpoint: flags.FlagToStringPointer(p, cmd, dremioTokenEndpointFlag), + DremioToken: flags.FlagToStringPointer(p, cmd, dremioPatFlag), + } + + // Check if any optional flag was provided + if model.DisplayName == nil && model.Description == nil && model.Labels == nil && + model.CatalogURI == nil && model.CatalogWarehouse == nil && model.CatalogNamespace == nil && + model.CatalogTableName == nil && model.CatalogAuthType == nil && + model.DremioTokenEndpoint == nil && model.DremioToken == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiUpdateIntakeRequest { + req := apiClient.DefaultAPI.UpdateIntake(ctx, model.ProjectId, model.Region, model.IntakeId) + + payload := intake.UpdateIntakePayload{ + IntakeRunnerId: utils.PtrString(model.RunnerId), // This is required by the API + DisplayName: model.DisplayName, + Description: model.Description, + Labels: utils.PtrValue(model.Labels), + } + + // Build catalog patch payload only if catalog-related flags are set + catalogPatch := &intake.IntakeCatalogPatch{} + catalogNeedsPatching := false + + if model.CatalogURI != nil { + catalogPatch.Uri = model.CatalogURI + catalogNeedsPatching = true + } + if model.CatalogWarehouse != nil { + catalogPatch.Warehouse = model.CatalogWarehouse + catalogNeedsPatching = true + } + if model.CatalogNamespace != nil { + catalogPatch.Namespace = model.CatalogNamespace + catalogNeedsPatching = true + } + if model.CatalogTableName != nil { + catalogPatch.TableName = model.CatalogTableName + catalogNeedsPatching = true + } + + // Build auth patch payload only if auth-related flags are set + authPatch := &intake.CatalogAuthPatch{} + authNeedsPatching := false + + if model.CatalogAuthType != nil { + authType := intake.CatalogAuthType(*model.CatalogAuthType) + authPatch.Type = &authType + authNeedsPatching = true + } + + dremioPatch := &intake.DremioAuthPatch{} + dremioNeedsPatching := false + if model.DremioTokenEndpoint != nil { + dremioPatch.TokenEndpoint = model.DremioTokenEndpoint + dremioNeedsPatching = true + } + if model.DremioToken != nil { + dremioPatch.PersonalAccessToken = model.DremioToken + dremioNeedsPatching = true + } + + if dremioNeedsPatching { + authPatch.Dremio = dremioPatch + authNeedsPatching = true + } + + if authNeedsPatching { + catalogPatch.Auth = authPatch + catalogNeedsPatching = true + } + + if catalogNeedsPatching { + payload.Catalog = catalogPatch + } + + req = req.UpdateIntakePayload(payload) + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeResponse) error { + return p.OutputResult(model.OutputFormat, resp, func() error { + if resp == nil { + p.Outputf("Updated Intake for project %q, but no intake ID was returned.\n", projectLabel) + return nil + } + + operationState := "Updated" + if model.Async { + operationState = "Triggered update of" + } + p.Outputf("%s Intake for project %q. Intake ID: %s\n", operationState, projectLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/intake/update/update_test.go b/internal/cmd/beta/intake/update/update_test.go new file mode 100644 index 000000000..4b3aaeb3a --- /dev/null +++ b/internal/cmd/beta/intake/update/update_test.go @@ -0,0 +1,289 @@ +package update + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() + testRunnerId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{testIntakeId} + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + runnerIdFlag: testRunnerId, + displayNameFlag: "new-display-name", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: testIntakeId, + RunnerId: utils.Ptr(testRunnerId), + DisplayName: utils.Ptr("new-display-name"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no optional flags provided", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + runnerIdFlag: testRunnerId, + }, + isValid: false, + }, + { + description: "update all fields", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[descriptionFlag] = "new description" + flagValues[labelsFlag] = "env=prod,team=sre" + flagValues[catalogURIFlag] = "new-uri" + flagValues[catalogWarehouseFlag] = "new-warehouse" + flagValues[catalogNamespaceFlag] = "new-namespace" + flagValues[catalogTableNameFlag] = "new-table" + flagValues[catalogAuthTypeFlag] = "dremio" + flagValues[dremioTokenEndpointFlag] = "new-endpoint" + flagValues[dremioPatFlag] = "new-pat" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("new description") + model.Labels = utils.Ptr(map[string]string{"env": "prod", "team": "sre"}) + model.CatalogURI = utils.Ptr("new-uri") + model.CatalogWarehouse = utils.Ptr("new-warehouse") + model.CatalogNamespace = utils.Ptr("new-namespace") + model.CatalogTableName = utils.Ptr("new-table") + model.CatalogAuthType = utils.Ptr("dremio") + model.DremioTokenEndpoint = utils.Ptr("new-endpoint") + model.DremioToken = utils.Ptr("new-pat") + }), + }, + { + description: "no args", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "runner-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, runnerIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedReq intake.ApiUpdateIntakeRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedReq: testClient.DefaultAPI.UpdateIntake(testCtx, testProjectId, testRegion, testIntakeId). + UpdateIntakePayload(intake.UpdateIntakePayload{ + IntakeRunnerId: testRunnerId, + DisplayName: utils.Ptr("new-display-name"), + }), + }, + { + description: "update description and catalog uri", + model: fixtureInputModel(func(model *inputModel) { + model.DisplayName = nil + model.Description = utils.Ptr("new-desc") + model.CatalogURI = utils.Ptr("new-uri") + }), + expectedReq: testClient.DefaultAPI.UpdateIntake(testCtx, testProjectId, testRegion, testIntakeId). + UpdateIntakePayload(intake.UpdateIntakePayload{ + IntakeRunnerId: testRunnerId, + Description: utils.Ptr("new-desc"), + Catalog: &intake.IntakeCatalogPatch{ + Uri: utils.Ptr("new-uri"), + }, + }), + }, + { + description: "update all fields", + model: fixtureInputModel(func(model *inputModel) { + model.DisplayName = utils.Ptr("another-name") + model.Description = utils.Ptr("final-desc") + model.Labels = utils.Ptr(map[string]string{"a": "b"}) + model.CatalogURI = utils.Ptr("final-uri") + model.CatalogWarehouse = utils.Ptr("final-warehouse") + model.CatalogNamespace = utils.Ptr("final-namespace") + model.CatalogTableName = utils.Ptr("final-table") + model.CatalogAuthType = utils.Ptr("dremio") + model.DremioTokenEndpoint = utils.Ptr("final-endpoint") + model.DremioToken = utils.Ptr("final-token") + }), + expectedReq: testClient.DefaultAPI.UpdateIntake(testCtx, testProjectId, testRegion, testIntakeId). + UpdateIntakePayload(intake.UpdateIntakePayload{ + IntakeRunnerId: testRunnerId, + DisplayName: utils.Ptr("another-name"), + Description: utils.Ptr("final-desc"), + Labels: map[string]string{"a": "b"}, + Catalog: &intake.IntakeCatalogPatch{ + Uri: utils.Ptr("final-uri"), + Warehouse: utils.Ptr("final-warehouse"), + Namespace: utils.Ptr("final-namespace"), + TableName: utils.Ptr("final-table"), + Auth: &intake.CatalogAuthPatch{ + Type: utils.Ptr(intake.CatalogAuthType("dremio")), + Dremio: &intake.DremioAuthPatch{ + TokenEndpoint: utils.Ptr("final-endpoint"), + PersonalAccessToken: utils.Ptr("final-token"), + }, + }, + }, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedReq, request, + cmp.AllowUnexported(request), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + intakeId string + resp *intake.IntakeResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", projectLabel: "my-project", intakeId: "intake-id-123", resp: &intake.IntakeResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, resp: &intake.IntakeResponse{Id: "intake-id-123"}}, + wantErr: false, + }, + { + name: "yaml output", + args: args{outputFormat: print.YAMLOutputFormat, resp: &intake.IntakeResponse{Id: "runner-id-123"}}, + wantErr: false, + }, + { + name: "nil response", + args: args{outputFormat: print.JSONOutputFormat, resp: nil}, + wantErr: false, + }, + { + name: "nil response - default output", + args: args{outputFormat: "default", resp: nil}, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: tt.args.outputFormat}}, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/user/create/create.go b/internal/cmd/beta/intake/user/create/create.go new file mode 100644 index 000000000..83fc8ffb9 --- /dev/null +++ b/internal/cmd/beta/intake/user/create/create.go @@ -0,0 +1,175 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + displayNameFlag = "display-name" + intakeIdFlag = "intake-id" + passwordFlag = "password" + userTypeFlag = "type" + descriptionFlag = "description" + labelsFlag = "labels" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + DisplayName *string + IntakeId *string + Password *string + UserType *string + Description *string + Labels *map[string]string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new Intake User", + Long: "Creates a new Intake User for a specific Intake.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new Intake User with required parameters`, + `$ stackit beta intake user create --display-name intake-user --intake-id xxx --password "SuperSafepass123\!"`), + examples.NewExample( + `Create a new Intake User for the dead-letter queue with labels`, + `$ stackit beta intake user create --display-name dlq-user --intake-id xxx --password "SuperSafepass123\!" --type dead-letter --labels "env=prod"`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create an Intake User for project %q?", projectLabel) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Intake User: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Creating STACKIT Intake User", func() error { + _, err = wait.CreateOrUpdateIntakeUserWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.IntakeId, resp.GetId()).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Intake User creation: %w", err) + } + } + + return outputResult(p.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd, p) + return cmd +} + +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "The UUID of the Intake to associate the user with") + password := flags.SecretFlag(passwordFlag, params) + cmd.Flags().Var(password, passwordFlag, password.Usage()+" Must contain lower, upper, number, and special characters (min 12 chars)") + cmd.Flags().String(userTypeFlag, string(intake.USERTYPE_INTAKE), "Type of user. One of 'intake' (default) or 'dead-letter'") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringToString(labelsFlag, nil, "Labels in key=value format, separated by commas") + + err := flags.MarkFlagsRequired(cmd, displayNameFlag, intakeIdFlag, passwordFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + IntakeId: flags.FlagToStringPointer(p, cmd, intakeIdFlag), + Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag), + UserType: flags.FlagToStringPointer(p, cmd, userTypeFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiCreateIntakeUserRequest { + req := apiClient.DefaultAPI.CreateIntakeUser(ctx, model.ProjectId, model.Region, *model.IntakeId) + + var userType *intake.UserType + if model.UserType != nil { + userType = utils.Ptr(intake.UserType(*model.UserType)) + } + + payload := intake.CreateIntakeUserPayload{ + DisplayName: utils.PtrString(model.DisplayName), + Password: utils.PtrString(model.Password), + Type: userType, + Description: model.Description, + Labels: utils.PtrValue(model.Labels), + } + + req = req.CreateIntakeUserPayload(payload) + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *intake.IntakeUserResponse) error { + return p.OutputResult(model.OutputFormat, resp, func() error { + if resp == nil { + p.Outputf("Triggered creation of Intake User for project %q, but no user ID was returned.\n", projectLabel) + return nil + } + + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s Intake User for project %q. User ID: %s\n", operationState, projectLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/intake/user/create/create_test.go b/internal/cmd/beta/intake/user/create/create_test.go new file mode 100644 index 000000000..e8babf6d0 --- /dev/null +++ b/internal/cmd/beta/intake/user/create/create_test.go @@ -0,0 +1,297 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// Define a unique key for the context to avoid collisions +type testCtxKey struct{} + +const ( + testRegion = "eu01" + testDisplayName = "testuser" + testPassword = "Secret12345!" + testUserType = "intake" + testDescription = "This is a test user" + testLabelsString = "env=test,team=dev" +) + +var ( + // testCtx dummy context for testing purposes + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + // testClient mock API client + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() + + testLabels = map[string]string{"env": "test", "team": "dev"} +) + +// fixtureFlagValues generates a map of flag values for tests +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + intakeIdFlag: testIntakeId, + passwordFlag: testPassword, + userTypeFlag: testUserType, + descriptionFlag: testDescription, + labelsFlag: testLabelsString, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// fixtureInputModel generates an input model for tests +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: utils.Ptr(testDisplayName), + IntakeId: utils.Ptr(testIntakeId), + Password: utils.Ptr(testPassword), + UserType: utils.Ptr(testUserType), + Description: utils.Ptr(testDescription), + Labels: utils.Ptr(testLabels), + } + for _, mod := range mods { + mod(model) + } + return model +} + +// fixtureCreatePayload generates a CreateIntakeUserPayload for tests +func fixtureCreatePayload(mods ...func(payload *intake.CreateIntakeUserPayload)) intake.CreateIntakeUserPayload { + userType := intake.UserType(testUserType) + payload := intake.CreateIntakeUserPayload{ + DisplayName: testDisplayName, + Password: testPassword, + Type: &userType, + Description: utils.Ptr(testDescription), + Labels: testLabels, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +// fixtureRequest generates an API request for tests +func fixtureRequest(mods ...func(request *intake.ApiCreateIntakeUserRequest)) intake.ApiCreateIntakeUserRequest { + request := testClient.DefaultAPI.CreateIntakeUser(testCtx, testProjectId, testRegion, testIntakeId) + request = request.CreateIntakeUserPayload(fixtureCreatePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, intakeIdFlag) + }), + isValid: false, + }, + { + description: "display name missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + { + description: "password missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, passwordFlag) + }), + isValid: false, + }, + { + description: "required fields only", + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + intakeIdFlag: testIntakeId, + passwordFlag: testPassword, + userTypeFlag: testUserType, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.Labels = nil + // UserType has a default value in the command definition, so it should still be populated + model.UserType = utils.Ptr(string(intake.USERTYPE_INTAKE)) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiCreateIntakeUserRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no optionals", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.Labels = nil + model.UserType = nil + }), + expectedRequest: fixtureRequest(func(request *intake.ApiCreateIntakeUserRequest) { + *request = request.CreateIntakeUserPayload(fixtureCreatePayload(func(payload *intake.CreateIntakeUserPayload) { + payload.Description = nil + payload.Labels = nil + payload.Type = nil + })) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + projectLabel string + resp *intake.IntakeUserResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{ + model: fixtureInputModel(), + projectLabel: "my-project", + resp: &intake.IntakeUserResponse{Id: "user-id-123"}, + }, + wantErr: false, + }, + { + name: "default output - async", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.Async = true + }), + projectLabel: "my-project", + resp: &intake.IntakeUserResponse{Id: "user-id-123"}, + }, + wantErr: false, + }, + { + name: "json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: &intake.IntakeUserResponse{Id: "user-id-123"}, + }, + wantErr: false, + }, + { + name: "nil response - default output", + args: args{ + model: fixtureInputModel(), + resp: nil, + }, + wantErr: false, + }, + { + name: "nil response - json output", + args: args{ + model: fixtureInputModel(func(model *inputModel) { + model.OutputFormat = print.JSONOutputFormat + }), + resp: nil, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/user/delete/delete.go b/internal/cmd/beta/intake/user/delete/delete.go new file mode 100644 index 000000000..0916cd430 --- /dev/null +++ b/internal/cmd/beta/intake/user/delete/delete.go @@ -0,0 +1,126 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + userIdArg = "USER_ID" + intakeIdFlag = "intake-id" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + IntakeId string + UserId string +} + +// NewCmd creates a new cobra command for deleting an Intake User +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", userIdArg), + Short: "Deletes an Intake User", + Long: "Deletes an Intake User.", + Args: args.SingleArg(userIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete an Intake User with ID "xxx" for Intake "yyy"`, + `$ stackit beta intake user delete xxx --intake-id yyy`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + prompt := fmt.Sprintf("Are you sure you want to delete Intake User %q?", model.UserId) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + if err = req.Execute(); err != nil { + return fmt.Errorf("delete Intake User: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Deleting STACKIT Intake User", func() error { + _, err = wait.DeleteIntakeUserWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.IntakeId, model.UserId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for STACKIT Intake User deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + p.Printer.Outputf("%s STACKIT Intake User %s\n", operationState, model.UserId) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "Intake ID") + + err := flags.MarkFlagsRequired(cmd, intakeIdFlag) + cobra.CheckErr(err) +} + +// parseInput parses the command arguments and flags into a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + userId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: flags.FlagToStringValue(p, cmd, intakeIdFlag), + UserId: userId, + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to delete an Intake User +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiDeleteIntakeUserRequest { + req := apiClient.DefaultAPI.DeleteIntakeUser(ctx, model.ProjectId, model.Region, model.IntakeId, model.UserId) + return req +} diff --git a/internal/cmd/beta/intake/user/delete/delete_test.go b/internal/cmd/beta/intake/user/delete/delete_test.go new file mode 100644 index 000000000..f5e02d8ed --- /dev/null +++ b/internal/cmd/beta/intake/user/delete/delete_test.go @@ -0,0 +1,178 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +// Define a unique key for the context to avoid collisions +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + // testCtx is a dummy context for testing purposes + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + // testClient is a mock API client + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() + testUserId = uuid.NewString() +) + +// fixtureArgValues generates a slice of arguments for tests +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testUserId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// fixtureFlagValues generates a map of flag values for tests +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + intakeIdFlag: testIntakeId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// fixtureInputModel generates an input model for tests +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: testIntakeId, + UserId: testUserId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// fixtureRequest generates an API request for tests +func fixtureRequest(mods ...func(request *intake.ApiDeleteIntakeUserRequest)) intake.ApiDeleteIntakeUserRequest { + request := testClient.DefaultAPI.DeleteIntakeUser(testCtx, testProjectId, testRegion, testIntakeId, testUserId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, intakeIdFlag) + }), + isValid: false, + }, + { + description: "intake id invalid", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[intakeIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "user id invalid", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiDeleteIntakeUserRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/intake/user/describe/describe.go b/internal/cmd/beta/intake/user/describe/describe.go new file mode 100644 index 000000000..66c53cda2 --- /dev/null +++ b/internal/cmd/beta/intake/user/describe/describe.go @@ -0,0 +1,132 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + userIdArg = "USER_ID" + intakeIdFlag = "intake-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + IntakeId string + UserId string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", userIdArg), + Short: "Shows details of an Intake User", + Long: "Shows details of an Intake User.", + Args: args.SingleArg(userIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of an Intake User with ID "xxx" for Intake "yyy"`, + `$ stackit beta intake user describe xxx --intake-id yyy`), + examples.NewExample( + `Get details of an Intake User with ID "xxx" in JSON format`, + `$ stackit beta intake user describe xxx --intake-id yyy --output-format json`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get Intake User: %w", err) + } + + return outputResult(p.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "Intake ID") + + err := flags.MarkFlagsRequired(cmd, intakeIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + userId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: flags.FlagToStringValue(p, cmd, intakeIdFlag), + UserId: userId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiGetIntakeUserRequest { + req := apiClient.DefaultAPI.GetIntakeUser(ctx, model.ProjectId, model.Region, model.IntakeId, model.UserId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, user *intake.IntakeUserResponse) error { + if user == nil { + return fmt.Errorf("received nil response, could not display details") + } + + return p.OutputResult(outputFormat, user, func() error { + table := tables.NewTable() + table.SetHeader("Attribute", "Value") + + table.AddRow("ID", user.GetId()) + table.AddRow("Name", user.GetDisplayName()) + table.AddRow("State", user.GetState()) + table.AddRow("Type", user.Type) + + table.AddRow("Username", user.GetUser()) + table.AddRow("Created", user.GetCreateTime()) + table.AddRow("Labels", user.GetLabels()) + + if description := user.GetDescription(); description != "" { + table.AddRow("Description", description) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/beta/intake/user/describe/describe_test.go b/internal/cmd/beta/intake/user/describe/describe_test.go new file mode 100644 index 000000000..75ea5bd05 --- /dev/null +++ b/internal/cmd/beta/intake/user/describe/describe_test.go @@ -0,0 +1,215 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() + testUserId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testUserId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + intakeIdFlag: testIntakeId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: testIntakeId, + UserId: testUserId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiGetIntakeUserRequest)) intake.ApiGetIntakeUserRequest { + request := testClient.DefaultAPI.GetIntakeUser(testCtx, testProjectId, testRegion, testIntakeId, testUserId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, intakeIdFlag) + }), + isValid: false, + }, + { + description: "intake id invalid", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[intakeIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "user id invalid", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiGetIntakeUserRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + user *intake.IntakeUserResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", user: &intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, user: &intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "yaml output", + args: args{outputFormat: print.YAMLOutputFormat, user: &intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "nil user", + args: args{user: nil}, + wantErr: true, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.user); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/user/list/list.go b/internal/cmd/beta/intake/user/list/list.go new file mode 100644 index 000000000..a81e10c53 --- /dev/null +++ b/internal/cmd/beta/intake/user/list/list.go @@ -0,0 +1,157 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const ( + intakeIdFlag = "intake-id" + limitFlag = "limit" +) + +// inputModel struct holds all the input parameters for the command +type inputModel struct { + *globalflags.GlobalFlagModel + IntakeId *string + Limit *int64 +} + +// NewCmd creates a new cobra command for listing Intake Users +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all Intake Users", + Long: "Lists all Intake Users for a specific Intake.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all users for an Intake`, + `$ stackit beta intake user list --intake-id xxx`), + examples.NewExample( + `List all users for an Intake in JSON format`, + `$ stackit beta intake user list --intake-id xxx --output-format json`), + examples.NewExample( + `List up to 5 users for an Intake`, + `$ stackit beta intake user list --intake-id xxx --limit 5`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list Intake Users: %w", err) + } + users := resp.GetIntakeUsers() + + // Truncate output + if model.Limit != nil && len(users) > int(*model.Limit) { + users = users[:*model.Limit] + } + + projectLabel := model.ProjectId + if len(users) == 0 { + projectLabel, err = projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + } + } + + return outputResult(p.Printer, model.OutputFormat, projectLabel, *model.IntakeId, users) + }, + } + configureFlags(cmd) + return cmd +} + +// configureFlags adds the flags to the command +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "Intake ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, intakeIdFlag) + cobra.CheckErr(err) +} + +// parseInput parses the command flags into a standardized model +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: flags.FlagToStringPointer(p, cmd, intakeIdFlag), + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +// buildRequest creates the API request to list Intake Users +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiListIntakeUsersRequest { + req := apiClient.DefaultAPI.ListIntakeUsers(ctx, model.ProjectId, model.Region, *model.IntakeId) + return req +} + +// outputResult formats the API response and prints it to the console +func outputResult(p *print.Printer, outputFormat, projectLabel, intakeId string, users []intake.IntakeUserResponse) error { + return p.OutputResult(outputFormat, users, func() error { + if len(users) == 0 { + p.Outputf("No intake users found for intake %q in project %q\n", intakeId, projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "DISPLAY NAME", "TYPE", "STATE") + for _, user := range users { + table.AddRow( + user.GetId(), + user.GetDisplayName(), + user.Type, + user.GetState(), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/intake/user/list/list_test.go b/internal/cmd/beta/intake/user/list/list_test.go new file mode 100644 index 000000000..2317c6124 --- /dev/null +++ b/internal/cmd/beta/intake/user/list/list_test.go @@ -0,0 +1,229 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() + testLimit = int64(5) +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + intakeIdFlag: testIntakeId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: utils.Ptr(testIntakeId), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiListIntakeUsersRequest)) intake.ApiListIntakeUsersRequest { + request := testClient.DefaultAPI.ListIntakeUsers(testCtx, testProjectId, testRegion, testIntakeId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "with limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = strconv.FormatInt(testLimit, 10) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = utils.Ptr(testLimit) + }), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, intakeIdFlag) + }), + isValid: false, + }, + { + description: "intake id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[intakeIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit is zero", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "limit is negative", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(p, cmd) + }, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest intake.ApiListIntakeUsersRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreUnexported(intake.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + intakeId string + users []intake.IntakeUserResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", intakeId: testIntakeId, users: []intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, intakeId: testIntakeId, users: []intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "empty slice", + args: args{intakeId: testIntakeId, users: []intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "nil slice", + args: args{intakeId: testIntakeId, users: nil}, + wantErr: false, + }, + { + name: "empty user in slice", + args: args{ + intakeId: testIntakeId, + users: []intake.IntakeUserResponse{{}}, + }, + wantErr: false, + }, + { + name: "with project label", + args: args{ + projectLabel: "my-project", + intakeId: testIntakeId, + users: []intake.IntakeUserResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.intakeId, tt.args.users); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/user/update/update.go b/internal/cmd/beta/intake/user/update/update.go new file mode 100644 index 000000000..4e1fc9d73 --- /dev/null +++ b/internal/cmd/beta/intake/user/update/update.go @@ -0,0 +1,171 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/intake/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + userIdArg = "USER_ID" + + intakeIdFlag = "intake-id" + displayNameFlag = "display-name" + descriptionFlag = "description" + passwordFlag = "password" + userTypeFlag = "type" + labelsFlag = "labels" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + IntakeId string + UserId string + DisplayName *string + Description *string + Password *string + UserType *string + Labels *map[string]string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", userIdArg), + Short: "Updates an Intake User", + Long: "Updates an Intake User. Only the specified fields are updated.", + Args: args.SingleArg(userIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the display name of an Intake User`, + `$ stackit beta intake user update xxx --intake-id yyy --display-name "new-user-name"`), + examples.NewExample( + `Update the password and description for an Intake User`, + `$ stackit beta intake user update xxx --intake-id yyy --password "NewSecret123\!" --description "Updated description"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update Intake User: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(p.Printer, "Updating STACKIT Intake User", func() error { + _, err = wait.CreateOrUpdateIntakeUserWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.IntakeId, model.UserId).WaitWithContext(ctx) + return err + }) + + if err != nil { + return fmt.Errorf("wait for STACKIT Intake User update: %w", err) + } + } + + return outputResult(p.Printer, model, resp) + }, + } + configureFlags(cmd, p) + return cmd +} + +func configureFlags(cmd *cobra.Command, p *types.CmdParams) { + cmd.Flags().Var(flags.UUIDFlag(), intakeIdFlag, "Intake ID") + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().String(descriptionFlag, "", "Description") + password := flags.SecretFlag(passwordFlag, p) + cmd.Flags().Var(password, passwordFlag, password.Usage()+" Must contain lower, upper, number, and special characters (min 12 chars)") + cmd.Flags().String(userTypeFlag, "", "Type of user. One of 'intake' or 'dead-letter'") + cmd.Flags().StringToString(labelsFlag, nil, `Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2".`) + + err := flags.MarkFlagsRequired(cmd, intakeIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + userId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := &inputModel{ + GlobalFlagModel: globalFlags, + IntakeId: flags.FlagToStringValue(p, cmd, intakeIdFlag), + UserId: userId, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag), + UserType: flags.FlagToStringPointer(p, cmd, userTypeFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + } + + if model.DisplayName == nil && model.Description == nil && model.Password == nil && model.UserType == nil && model.Labels == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *intake.APIClient) intake.ApiUpdateIntakeUserRequest { + req := apiClient.DefaultAPI.UpdateIntakeUser(ctx, model.ProjectId, model.Region, model.IntakeId, model.UserId) + + payload := intake.UpdateIntakeUserPayload{ + DisplayName: model.DisplayName, + Description: model.Description, + Password: model.Password, + Labels: utils.PtrValue(model.Labels), + } + + if model.UserType != nil { + userType := intake.UserType(*model.UserType) + payload.Type = &userType + } + + req = req.UpdateIntakeUserPayload(payload) + return req +} + +func outputResult(p *print.Printer, model *inputModel, resp *intake.IntakeUserResponse) error { + return p.OutputResult(model.OutputFormat, resp, func() error { + if resp == nil { + p.Outputf("Triggered update of Intake User for intake %q, but no user ID was returned.\n", model.IntakeId) + return nil + } + + operationState := "Updated" + if model.Async { + operationState = "Triggered update of" + } + p.Outputf("%s Intake User for intake %q. User ID: %s\n", operationState, model.IntakeId, resp.Id) + return nil + }) +} diff --git a/internal/cmd/beta/intake/user/update/update_test.go b/internal/cmd/beta/intake/user/update/update_test.go new file mode 100644 index 000000000..4364a1b3c --- /dev/null +++ b/internal/cmd/beta/intake/user/update/update_test.go @@ -0,0 +1,263 @@ +package update + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &intake.APIClient{ + DefaultAPI: &intake.DefaultAPIService{}, + } + testProjectId = uuid.NewString() + testIntakeId = uuid.NewString() + testUserId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{testUserId} + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + intakeIdFlag: testIntakeId, + displayNameFlag: "new-display-name", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + IntakeId: testIntakeId, + UserId: testUserId, + DisplayName: utils.Ptr("new-display-name"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *intake.ApiUpdateIntakeUserRequest)) intake.ApiUpdateIntakeUserRequest { + request := testClient.DefaultAPI.UpdateIntakeUser(testCtx, testProjectId, testRegion, testIntakeId, testUserId) + payload := intake.UpdateIntakeUserPayload{ + DisplayName: utils.Ptr("new-display-name"), + } + request = request.UpdateIntakeUserPayload(payload) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no optional flags provided", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + intakeIdFlag: testIntakeId, + }, + isValid: false, + }, + { + description: "update all fields", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[descriptionFlag] = "new description" + flagValues[labelsFlag] = "env=prod,team=sre" + flagValues[userTypeFlag] = "dead-letter" + flagValues[passwordFlag] = "NewSecret123!" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = utils.Ptr("new description") + model.Labels = utils.Ptr(map[string]string{"env": "prod", "team": "sre"}) + model.UserType = utils.Ptr("dead-letter") + model.Password = utils.Ptr("NewSecret123!") + }), + }, + { + description: "no args", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "intake-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, intakeIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedReq intake.ApiUpdateIntakeUserRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedReq: fixtureRequest(), + }, + { + description: "update description", + model: fixtureInputModel(func(model *inputModel) { + model.DisplayName = nil + model.Description = utils.Ptr("new-desc") + }), + expectedReq: fixtureRequest(func(request *intake.ApiUpdateIntakeUserRequest) { + payload := intake.UpdateIntakeUserPayload{ + Description: utils.Ptr("new-desc"), + } + *request = request.UpdateIntakeUserPayload(payload) + }), + }, + { + description: "update all fields", + model: fixtureInputModel(func(model *inputModel) { + model.DisplayName = utils.Ptr("another-name") + model.Description = utils.Ptr("final-desc") + model.Labels = utils.Ptr(map[string]string{"a": "b"}) + model.UserType = utils.Ptr("dead-letter") + model.Password = utils.Ptr("Secret123!") + }), + expectedReq: fixtureRequest(func(request *intake.ApiUpdateIntakeUserRequest) { + userType := intake.UserType("dead-letter") + payload := intake.UpdateIntakeUserPayload{ + DisplayName: utils.Ptr("another-name"), + Description: utils.Ptr("final-desc"), + Labels: map[string]string{"a": "b"}, + Type: &userType, + Password: utils.Ptr("Secret123!"), + } + *request = request.UpdateIntakeUserPayload(payload) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedReq, request, + cmp.AllowUnexported(request), + cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreUnexported(intake.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + intakeId string + resp *intake.IntakeUserResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", projectLabel: "my-project", intakeId: "intake-id-123", resp: &intake.IntakeUserResponse{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, resp: &intake.IntakeUserResponse{Id: "user-id-123"}}, + wantErr: false, + }, + { + name: "nil response", + args: args{outputFormat: print.JSONOutputFormat, resp: nil}, + wantErr: false, + }, + { + name: "nil response - default output", + args: args{outputFormat: "default", resp: nil}, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: tt.args.outputFormat}, IntakeId: tt.args.intakeId}, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/intake/user/user.go b/internal/cmd/beta/intake/user/user.go new file mode 100644 index 000000000..6e53c457b --- /dev/null +++ b/internal/cmd/beta/intake/user/user.go @@ -0,0 +1,36 @@ +package user + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/user/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/user/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/user/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/user/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/intake/user/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "user", + Short: "Provides functionality for Intake Users", + Long: "Provides functionality for Intake Users.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + // Pass the params down to each action command + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/export-policy/create/create.go b/internal/cmd/beta/sfs/export-policy/create/create.go new file mode 100644 index 000000000..1dce5b646 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/create/create.go @@ -0,0 +1,150 @@ +package create + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + nameFlag = "name" + rulesFlag = "rules" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string + Rules []sfs.CreateShareExportPolicyRequestRule +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates an export policy", + Long: "Creates an export policy.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new export policy with name "EXPORT_POLICY_NAME"`, + "$ stackit beta sfs export-policy create --name EXPORT_POLICY_NAME", + ), + examples.NewExample( + `Create a new export policy with name "EXPORT_POLICY_NAME" and rules from file "./rules.json"`, + "$ stackit beta sfs export-policy create --name EXPORT_POLICY_NAME --rules @./rules.json", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a export policy for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create export policy: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Export policy name") + cmd.Flags().Var(flags.ReadFromFileFlag(), rulesFlag, "Rules of the export policy (format: json)") + + err := flags.MarkFlagsRequired(cmd, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + rulesString := flags.FlagToStringValue(p, cmd, rulesFlag) + var rules []sfs.CreateShareExportPolicyRequestRule + if rulesString != "" { + var r []sfs.CreateShareExportPolicyRequestRule + err := json.Unmarshal([]byte(rulesString), &r) + if err != nil { + return nil, fmt.Errorf("could not parse rules: %w", err) + } + rules = r + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + Rules: rules, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiCreateShareExportPolicyRequest { + req := apiClient.DefaultAPI.CreateShareExportPolicy(ctx, model.ProjectId, model.Region) + req = req.CreateShareExportPolicyPayload( + sfs.CreateShareExportPolicyPayload{ + Name: model.Name, + Rules: model.Rules, + }, + ) + return req +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, item *sfs.CreateShareExportPolicyResponse) error { + return p.OutputResult(outputFormat, item, func() error { + if item == nil || item.ShareExportPolicy == nil { + return fmt.Errorf("no export policy found") + } + p.Outputf( + "Created export policy %q for project %q.\nExport policy ID: %s\n", + utils.PtrString(item.ShareExportPolicy.Name), + projectLabel, + utils.PtrString(item.ShareExportPolicy.Id), + ) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/export-policy/create/create_test.go b/internal/cmd/beta/sfs/export-policy/create/create_test.go new file mode 100644 index 000000000..0715ca337 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/create/create_test.go @@ -0,0 +1,212 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" +var testName = "test-name" +var testRulesString = "[]" +var testRules = []sfs.CreateShareExportPolicyRequestRule{} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + nameFlag: testName, + rulesFlag: testRulesString, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: testName, + Rules: testRules, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request sfs.ApiCreateShareExportPolicyRequest)) sfs.ApiCreateShareExportPolicyRequest { + request := testClient.DefaultAPI.CreateShareExportPolicy(testCtx, testProjectId, testRegion) + request = request.CreateShareExportPolicyPayload(fixturePayload()) + for _, mod := range mods { + mod(request) + } + return request +} + +func fixturePayload(mods ...func(payload *sfs.CreateShareExportPolicyPayload)) sfs.CreateShareExportPolicyPayload { + payload := sfs.CreateShareExportPolicyPayload{ + Name: testName, + Rules: testRules, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "required only", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, rulesFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Rules = nil + }), + }, + { + description: "required read rules from file", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[rulesFlag] = "@../test-files/rules-example.json" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Rules = []sfs.CreateShareExportPolicyRequestRule{ + { + Description: *sfs.NewNullableString(utils.Ptr("first rule")), + IpAcl: []string{"192.168.2.0/24"}, + Order: utils.Ptr(int32(1)), + SetUuid: utils.Ptr(true), + SuperUser: utils.Ptr(false), + AdditionalProperties: map[string]any{}, + }, + { + IpAcl: []string{"192.168.2.0/24", "127.0.0.1/32"}, + Order: utils.Ptr(int32(2)), + ReadOnly: utils.Ptr(true), + AdditionalProperties: map[string]any{"readonly": true}, + }, + } + }), + }, + } + opts := []testutils.TestingOption{ + testutils.WithCmpOptions(cmp.AllowUnexported(sfs.NullableString{})), + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInputWithOptions(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, nil, tt.isValid, opts) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiCreateShareExportPolicyRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + exportPolicy *sfs.CreateShareExportPolicyResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "set empty export policy", + args: args{ + exportPolicy: &sfs.CreateShareExportPolicyResponse{}, + }, + wantErr: true, + }, + { + name: "set empty export policy", + args: args{ + exportPolicy: &sfs.CreateShareExportPolicyResponse{ + ShareExportPolicy: &sfs.ShareExportPolicy{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.exportPolicy); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/export-policy/delete/delete.go b/internal/cmd/beta/sfs/export-policy/delete/delete.go new file mode 100644 index 000000000..35ce9a442 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/delete/delete.go @@ -0,0 +1,99 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const exportPolicyIdArg = "EXPORT_POLICY_ID" + +type inputModel struct { + *globalflags.GlobalFlagModel + ExportPolicyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", exportPolicyIdArg), + Short: "Deletes an export policy", + Long: "Deletes an export policy.", + Args: args.SingleArg(exportPolicyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete an export policy with ID "xxx"`, + "$ stackit beta sfs export-policy delete xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + exportPolicyLabel, err := sfsUtils.GetExportPolicyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ExportPolicyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get export policy name: %v", err) + exportPolicyLabel = model.ExportPolicyId + } else if exportPolicyLabel == "" { + exportPolicyLabel = model.ExportPolicyId + } + + prompt := fmt.Sprintf("Are you sure you want to delete export policy %q? (This cannot be undone)", exportPolicyLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete export policy: %w", err) + } + + params.Printer.Outputf("Deleted export policy %q\n", exportPolicyLabel) + return nil + }, + } + return cmd +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiDeleteShareExportPolicyRequest { + return apiClient.DefaultAPI.DeleteShareExportPolicy(ctx, model.ProjectId, model.Region, model.ExportPolicyId) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + exportPolicyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ExportPolicyId: exportPolicyId, + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/beta/sfs/export-policy/delete/delete_test.go b/internal/cmd/beta/sfs/export-policy/delete/delete_test.go new file mode 100644 index 000000000..418df1aa2 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/delete/delete_test.go @@ -0,0 +1,175 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" +var testExportPolicyId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testExportPolicyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ExportPolicyId: testExportPolicyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiDeleteShareExportPolicyRequest)) sfs.ApiDeleteShareExportPolicyRequest { + request := testClient.DefaultAPI.DeleteShareExportPolicy(testCtx, testProjectId, testRegion, testExportPolicyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "export policy id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "export policy id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiDeleteShareExportPolicyRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/export-policy/describe/describe.go b/internal/cmd/beta/sfs/export-policy/describe/describe.go new file mode 100644 index 000000000..6f29161cf --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/describe/describe.go @@ -0,0 +1,152 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const exportPolicyIdArg = "EXPORT_POLICY_ID" + +type inputModel struct { + *globalflags.GlobalFlagModel + ExportPolicyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", exportPolicyIdArg), + Short: "Shows details of an export policy", + Long: "Shows details of an export policy.", + Args: args.SingleArg(exportPolicyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe an export policy with ID "xxx"`, + "$ stackit beta sfs export-policy describe xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read export policy: %w", err) + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, model.ExportPolicyId, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + exportPolicyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ExportPolicyId: exportPolicyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetShareExportPolicyRequest { + return apiClient.DefaultAPI.GetShareExportPolicy(ctx, model.ProjectId, model.Region, model.ExportPolicyId) +} + +func outputResult(p *print.Printer, outputFormat, exportPolicyId, projectLabel string, exportPolicy *sfs.GetShareExportPolicyResponse) error { + return p.OutputResult(outputFormat, exportPolicy, func() error { + if exportPolicy == nil || exportPolicy.ShareExportPolicy == nil { + p.Outputf("Export policy %q not found in project %q", exportPolicyId, projectLabel) + return nil + } + + var content []tables.Table + + table := tables.NewTable() + table.SetTitle("Export Policy") + policy := exportPolicy.ShareExportPolicy + + table.AddRow("ID", utils.PtrString(policy.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(policy.Name)) + table.AddSeparator() + table.AddRow("SHARES USING EXPORT POLICY", utils.PtrString(policy.SharesUsingExportPolicy)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(policy.CreatedAt)) + + content = append(content, table) + + if len(policy.Rules) > 0 { + rulesTable := tables.NewTable() + rulesTable.SetTitle("Rules") + + rulesTable.SetHeader("ID", "ORDER", "DESCRIPTION", "IP ACL", "READ ONLY", "SET UUID", "SUPER USER", "CREATED AT") + + for _, rule := range policy.Rules { + var description string + if rule.Description.IsSet() && *rule.Description.Get() != "" { + description = *rule.Description.Get() + } + rulesTable.AddRow( + rule.Id, + rule.Order, + description, + utils.JoinStringPtr(&rule.IpAcl, ", "), + rule.ReadOnly, + rule.SetUuid, + rule.SuperUser, + utils.ConvertTimePToDateTimeString(rule.CreatedAt), + ) + rulesTable.AddSeparator() + } + + content = append(content, rulesTable) + } + + if err := tables.DisplayTables(p, content); err != nil { + return fmt.Errorf("render tables: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/sfs/export-policy/describe/describe_test.go b/internal/cmd/beta/sfs/export-policy/describe/describe_test.go new file mode 100644 index 000000000..ee9bb0188 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/describe/describe_test.go @@ -0,0 +1,221 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" +var testExportPolicyId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testExportPolicyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ExportPolicyId: testExportPolicyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetShareExportPolicyRequest)) sfs.ApiGetShareExportPolicyRequest { + request := testClient.DefaultAPI.GetShareExportPolicy(testCtx, testProjectId, testRegion, testExportPolicyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "export policy id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "export policy id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetShareExportPolicyRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + exportPolicyId string + projectLabel string + exportPolicy *sfs.GetShareExportPolicyResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty export policy", + args: args{ + exportPolicy: &sfs.GetShareExportPolicyResponse{}, + }, + wantErr: false, + }, + { + name: "set empty export policy", + args: args{ + exportPolicy: &sfs.GetShareExportPolicyResponse{ + ShareExportPolicy: &sfs.ShareExportPolicy{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.exportPolicyId, tt.args.projectLabel, tt.args.exportPolicy); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/export-policy/export-policy.go b/internal/cmd/beta/sfs/export-policy/export-policy.go new file mode 100644 index 000000000..221b4f1f3 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/export-policy.go @@ -0,0 +1,33 @@ +package exportpolicy + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/export-policy/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/export-policy/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/export-policy/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/export-policy/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/export-policy/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "export-policy", + Short: "Provides functionality for SFS export policies", + Long: "Provides functionality for SFS export policies.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/export-policy/list/list.go b/internal/cmd/beta/sfs/export-policy/list/list.go new file mode 100644 index 000000000..d3fd00883 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/list/list.go @@ -0,0 +1,148 @@ +package list + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all export policies of a project", + Long: "Lists all export policies of a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all export policies`, + "$ stackit beta sfs export-policy list", + ), + examples.NewExample( + `List up to 10 export policies`, + "$ stackit beta sfs export-policy list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list export policies: %w", err) + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.ShareExportPolicies) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiListShareExportPoliciesRequest { + return apiClient.DefaultAPI.ListShareExportPolicies(ctx, model.ProjectId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, exportPolicies []sfs.ShareExportPolicy) error { + return p.OutputResult(outputFormat, exportPolicies, func() error { + if len(exportPolicies) == 0 { + p.Outputf("No export policies found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "AMOUNT RULES", "SHARES USING THIS EXPORT POLICY", "CREATED AT") + + for _, exportPolicy := range exportPolicies { + amountRules := "-" + if exportPolicy.Rules != nil { + amountRules = strconv.Itoa(len(exportPolicy.Rules)) + } + table.AddRow( + utils.PtrString(exportPolicy.Id), + utils.PtrString(exportPolicy.Name), + amountRules, + utils.PtrString(exportPolicy.SharesUsingExportPolicy), + utils.ConvertTimePToDateTimeString(exportPolicy.CreatedAt), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/export-policy/list/list_test.go b/internal/cmd/beta/sfs/export-policy/list/list_test.go new file mode 100644 index 000000000..b2ce7389a --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/list/list_test.go @@ -0,0 +1,176 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + limitFlag: strconv.Itoa(10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListShareExportPoliciesRequest)) sfs.ApiListShareExportPoliciesRequest { + request := testClient.DefaultAPI.ListShareExportPolicies(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiListShareExportPoliciesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + exportPolicies []sfs.ShareExportPolicy + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty export policy", + args: args{ + exportPolicies: []sfs.ShareExportPolicy{ + {}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.exportPolicies); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/export-policy/test-files/rules-example.json b/internal/cmd/beta/sfs/export-policy/test-files/rules-example.json new file mode 100644 index 000000000..57b2cbcb1 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/test-files/rules-example.json @@ -0,0 +1,14 @@ +[ + { + "description": "first rule", + "ipAcl": ["192.168.2.0/24"], + "order": 1, + "superUser": false, + "setUuid": true + }, + { + "ipAcl": ["192.168.2.0/24", "127.0.0.1/32"], + "order": 2, + "readonly": true + } +] \ No newline at end of file diff --git a/internal/cmd/beta/sfs/export-policy/update/update.go b/internal/cmd/beta/sfs/export-policy/update/update.go new file mode 100644 index 000000000..55c22a5b8 --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/update/update.go @@ -0,0 +1,171 @@ +package update + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + exportPolicyArg = "EXPORT_POLICY_ID" + + rulesFlag = "rules" + removeRulesFlag = "remove-rules" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ExportPolicyId string + Rules *[]sfs.UpdateShareExportPolicyBodyRule +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", exportPolicyArg), + Short: "Updates an export policy", + Long: "Updates an export policy.", + Args: args.SingleArg(exportPolicyArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update an export policy with ID "xxx" and with rules from file "./rules.json"`, + "$ stackit beta sfs export-policy update xxx --rules @./rules.json", + ), + examples.NewExample( + `Update an export policy with ID "xxx" and remove the rules`, + "$ stackit beta sfs export-policy update XXX --remove-rules", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + exportPolicyLabel, err := sfsUtils.GetExportPolicyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ExportPolicyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get export policy name: %v", err) + exportPolicyLabel = model.ExportPolicyId + } else if exportPolicyLabel == "" { + exportPolicyLabel = model.ExportPolicyId + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to update export policy %q for project %q?", exportPolicyLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update export policy: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, exportPolicyLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.ReadFromFileFlag(), rulesFlag, "Rules of the export policy") + cmd.Flags().Bool(removeRulesFlag, false, "Remove the export policy rules") + + rulesFlags := []string{rulesFlag, removeRulesFlag} + cmd.MarkFlagsMutuallyExclusive(rulesFlags...) + cmd.MarkFlagsOneRequired(rulesFlags...) // Because the update endpoint supports only rules at the moment, one of the flags must be required +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiUpdateShareExportPolicyRequest { + req := apiClient.DefaultAPI.UpdateShareExportPolicy(ctx, model.ProjectId, model.Region, model.ExportPolicyId) + + payload := sfs.UpdateShareExportPolicyPayload{ + Rules: *model.Rules, + } + return req.UpdateShareExportPolicyPayload(payload) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + exportPolicyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + var rules *[]sfs.UpdateShareExportPolicyBodyRule + noRulesErr := fmt.Errorf("no rules specified") + if rulesString := flags.FlagToStringPointer(p, cmd, rulesFlag); rulesString != nil { + var r []sfs.UpdateShareExportPolicyBodyRule + err := json.Unmarshal([]byte(*rulesString), &r) + if err != nil { + return nil, fmt.Errorf("could not parse rules: %w", err) + } + if r == nil { + return nil, noRulesErr + } + rules = &r + } + + if removeRules := flags.FlagToBoolPointer(p, cmd, removeRulesFlag); removeRules != nil { + // Create an empty slice for the patch request + rules = &[]sfs.UpdateShareExportPolicyBodyRule{} + } + + // Because the update endpoint supports only rules at the moment, this should not be empty + if rules == nil { + return nil, noRulesErr + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ExportPolicyId: exportPolicyId, + Rules: rules, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, projectLabel, exportPolicyLabel string, resp *sfs.UpdateShareExportPolicyResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + p.Outputln("Empty export policy response") + return nil + } + p.Outputf("Updated export policy %q for project %q\n", exportPolicyLabel, projectLabel) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/export-policy/update/update_test.go b/internal/cmd/beta/sfs/export-policy/update/update_test.go new file mode 100644 index 000000000..a707cc54e --- /dev/null +++ b/internal/cmd/beta/sfs/export-policy/update/update_test.go @@ -0,0 +1,255 @@ +package update + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" + testRulesString = `[ + { + "ipAcl": ["172.16.0.0/24"], + "readOnly": true, + "order": 1 + } +]` +) + +var testRules = &[]sfs.UpdateShareExportPolicyBodyRule{ + { + IpAcl: []string{"172.16.0.0/24"}, + ReadOnly: utils.Ptr(true), + Order: utils.Ptr(int32(1)), + AdditionalProperties: map[string]any{}, + }, +} +var testExportPolicyId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + rulesFlag: testRulesString, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testExportPolicyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ExportPolicyId: testExportPolicyId, + Rules: testRules, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiUpdateShareExportPolicyRequest)) sfs.ApiUpdateShareExportPolicyRequest { + request := testClient.DefaultAPI.UpdateShareExportPolicy(testCtx, testProjectId, testRegion, testExportPolicyId) + request = request.UpdateShareExportPolicyPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *sfs.UpdateShareExportPolicyPayload)) sfs.UpdateShareExportPolicyPayload { + payload := sfs.UpdateShareExportPolicyPayload{ + Rules: *testRules, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no rules specified", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, rulesFlag) + }), + isValid: false, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Rules = nil + }), + }, + { + description: "conflict rules and remove rules", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[rulesFlag] = testRulesString + flagValues[removeRulesFlag] = "true" + }), + isValid: false, + }, + { + description: "--remove-rules flag set", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[removeRulesFlag] = "true" + delete(flagValues, rulesFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Rules = &[]sfs.UpdateShareExportPolicyBodyRule{} + }), + }, + { + description: "required read rules from file", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[rulesFlag] = "@../test-files/rules-example.json" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Rules = &[]sfs.UpdateShareExportPolicyBodyRule{ + { + Description: *sfs.NewNullableString( + utils.Ptr("first rule"), + ), + IpAcl: []string{"192.168.2.0/24"}, + Order: utils.Ptr(int32(1)), + SetUuid: utils.Ptr(true), + SuperUser: utils.Ptr(false), + AdditionalProperties: map[string]any{}, + }, + { + IpAcl: []string{"192.168.2.0/24", "127.0.0.1/32"}, + Order: utils.Ptr(int32(2)), + ReadOnly: utils.Ptr(true), + AdditionalProperties: map[string]any{"readonly": true}, + }, + } + }), + }, + } + opts := []testutils.TestingOption{ + testutils.WithCmpOptions(cmp.AllowUnexported(sfs.NullableString{})), + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInputWithOptions(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, nil, tt.isValid, opts) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiUpdateShareExportPolicyRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(sfs.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + exportPolicyLabel string + resp *sfs.UpdateShareExportPolicyResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty resp", + args: args{ + resp: &sfs.UpdateShareExportPolicyResponse{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.exportPolicyLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/performance-class/list/list.go b/internal/cmd/beta/sfs/performance-class/list/list.go new file mode 100644 index 000000000..2352c7411 --- /dev/null +++ b/internal/cmd/beta/sfs/performance-class/list/list.go @@ -0,0 +1,111 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all performances classes available", + Long: "Lists all performances classes available.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all performances classes`, + "$ stackit beta sfs performance-class list", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + resp, err := buildRequest(ctx, apiClient).Execute() + if err != nil { + return fmt.Errorf("list performance-class: %w", err) + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + performanceClasses := utils.GetSliceFromPointer(&resp.PerformanceClasses) + + return outputResult(params.Printer, model.OutputFormat, projectLabel, performanceClasses) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *sfs.APIClient) sfs.ApiListPerformanceClassesRequest { + return apiClient.DefaultAPI.ListPerformanceClasses(ctx) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, performanceClasses []sfs.PerformanceClass) error { + return p.OutputResult(outputFormat, performanceClasses, func() error { + if len(performanceClasses) == 0 { + p.Outputf("No performance classes found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("NAME", "IOPS", "THROUGHPUT") + for _, performanceClass := range performanceClasses { + table.AddRow( + utils.PtrString(performanceClass.Name), + utils.PtrString(performanceClass.Iops), + utils.PtrString(performanceClass.Throughput), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/performance-class/list/list_test.go b/internal/cmd/beta/sfs/performance-class/list/list_test.go new file mode 100644 index 000000000..1c05d3ec8 --- /dev/null +++ b/internal/cmd/beta/sfs/performance-class/list/list_test.go @@ -0,0 +1,171 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListPerformanceClassesRequest)) sfs.ApiListPerformanceClassesRequest { + request := testClient.DefaultAPI.ListPerformanceClasses(testCtx) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + expectedRequest sfs.ApiListPerformanceClassesRequest + }{ + { + description: "base", + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(sfs.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + performanceClasses []sfs.PerformanceClass + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty performance class", + args: args{ + performanceClasses: []sfs.PerformanceClass{ + {}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.performanceClasses); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/performance-class/performance_class.go b/internal/cmd/beta/sfs/performance-class/performance_class.go new file mode 100644 index 000000000..f033ee5d3 --- /dev/null +++ b/internal/cmd/beta/sfs/performance-class/performance_class.go @@ -0,0 +1,26 @@ +package performanceclass + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/performance-class/list" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "performance-class", + Short: "Provides functionality for SFS performance classes", + Long: "Provides functionality for SFS performance classes.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/project-lock/describe/describe.go b/internal/cmd/beta/sfs/project-lock/describe/describe.go new file mode 100644 index 000000000..5f6ce6624 --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/describe/describe.go @@ -0,0 +1,98 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Get lock status for a project", + Long: "Get lock status for a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Get lock status for project`, + "$ stackit beta sfs project-lock describe"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get lock status for project: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetLockRequest { + req := apiClient.DefaultAPI.GetLock(ctx, model.Region, model.ProjectId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, resp *sfs.GetLockResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("response is empty") + } + + table := tables.NewTable() + table.AddRow("LOCK ID", utils.PtrString(resp.LockId)) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/beta/sfs/project-lock/describe/describe_test.go b/internal/cmd/beta/sfs/project-lock/describe/describe_test.go new file mode 100644 index 000000000..9331cc3cc --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/describe/describe_test.go @@ -0,0 +1,178 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetLockRequest)) sfs.ApiGetLockRequest { + request := testClient.DefaultAPI.GetLock(testCtx, testRegion, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetLockRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLock *sfs.GetLockResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "projectLock is nil", + args: args{ + outputFormat: print.PrettyOutputFormat, + projectLock: nil, + }, + wantErr: true, + }, + { + name: "set empty project lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + projectLock: &sfs.GetLockResponse{}, + }, + wantErr: false, + }, + { + name: "set filled lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + projectLock: &sfs.GetLockResponse{ + LockId: utils.Ptr(uuid.New().String()), + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLock); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/project-lock/lock/lock.go b/internal/cmd/beta/sfs/project-lock/lock/lock.go new file mode 100644 index 000000000..a4d422894 --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/lock/lock.go @@ -0,0 +1,103 @@ +package lock + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "lock", + Short: "Enables lock for a project", + Long: "Enables lock for a project. Necessary for immutable snapshots and to prevent accidental deletion of resources.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Enable lock for project`, + "$ stackit beta sfs project-lock lock", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to enable SFS lock for project %s?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("enable SFS project lock: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiEnableLockRequest { + return apiClient.DefaultAPI.EnableLock(ctx, model.Region, model.ProjectId) +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *sfs.EnableLockResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("enable project lock response is empty") + } + + p.Outputf("Project %q is successfully locked.\n", projectLabel) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/project-lock/lock/lock_test.go b/internal/cmd/beta/sfs/project-lock/lock/lock_test.go new file mode 100644 index 000000000..224414bae --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/lock/lock_test.go @@ -0,0 +1,178 @@ +package lock + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiEnableLockRequest)) sfs.ApiEnableLockRequest { + request := testClient.DefaultAPI.EnableLock(testCtx, testRegion, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiEnableLockRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + projectLock *sfs.EnableLockResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: true, + }, + { + name: "set empty project lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + projectLock: &sfs.EnableLockResponse{}, + }, + wantErr: false, + }, + { + name: "set filled lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + projectLock: &sfs.EnableLockResponse{ + LockId: utils.Ptr(uuid.New().String()), + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.projectLock); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/project-lock/project-lock.go b/internal/cmd/beta/sfs/project-lock/project-lock.go new file mode 100644 index 000000000..df98ea76f --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/project-lock.go @@ -0,0 +1,30 @@ +package projectlock + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/project-lock/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/project-lock/lock" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/project-lock/unlock" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "project-lock", + Short: "Provides functionality for SFS project locks", + Long: "Provides functionality for SFS project locks.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(lock.NewCmd(params)) + cmd.AddCommand(unlock.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/project-lock/unlock/unlock.go b/internal/cmd/beta/sfs/project-lock/unlock/unlock.go new file mode 100644 index 000000000..fe0f4d38d --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/unlock/unlock.go @@ -0,0 +1,94 @@ +package unlock + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "unlock", + Short: "Clean up lock for a project", + Long: "Clean up lock for a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Disable lock for project`, + "$ stackit beta sfs project-lock unlock"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to disable lock for project %s?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + _, err = buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("disable project lock: %w", err) + } + + params.Printer.Outputf("Project %q is successfully unlocked.\n", projectLabel) + + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiDisableLockRequest { + req := apiClient.DefaultAPI.DisableLock(ctx, model.Region, model.ProjectId) + return req +} diff --git a/internal/cmd/beta/sfs/project-lock/unlock/unlock_test.go b/internal/cmd/beta/sfs/project-lock/unlock/unlock_test.go new file mode 100644 index 000000000..def8b77e1 --- /dev/null +++ b/internal/cmd/beta/sfs/project-lock/unlock/unlock_test.go @@ -0,0 +1,128 @@ +package unlock + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiDisableLockRequest)) sfs.ApiDisableLockRequest { + request := testClient.DefaultAPI.DisableLock(testCtx, testRegion, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiDisableLockRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/resource-pool/create/create.go b/internal/cmd/beta/sfs/resource-pool/create/create.go new file mode 100644 index 000000000..afbb83200 --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/create/create.go @@ -0,0 +1,196 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + performanceClassFlag = "performance-class" + sizeFlag = "size" + ipAclFlag = "ip-acl" + availabilityZoneFlag = "availability-zone" + nameFlag = "name" + snapshotsVisibleFlag = "snapshots-visible" + snapshotPolicyIdFlag = "snapshot-policy-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + SizeInGB *int32 + PerformanceClass string + IpAcl []string + Name string + AvailabilityZone string + SnapshotsVisible bool + SnapshotPolicyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a SFS resource pool", + Long: `Creates a SFS resource pool. + +The available performance class values can be obtained by running: + $ stackit beta sfs performance-class list`, + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a SFS resource pool`, + "$ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 10.88.135.144/28 --performance-class Standard --size 500 --name resource-pool-01"), + examples.NewExample( + `Create a SFS resource pool, allow only a single IP which can mount the resource pool`, + "$ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 250.81.87.224/32 --performance-class Standard --size 500 --name resource-pool-01"), + examples.NewExample( + `Create a SFS resource pool, allow multiple IP ACL which can mount the resource pool`, + "$ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl \"10.88.135.144/28,250.81.87.224/32\" --performance-class Standard --size 500 --name resource-pool-01"), + examples.NewExample( + `Create a SFS resource pool with visible snapshots`, + "$ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 10.88.135.144/28 --performance-class Standard --size 500 --name resource-pool-01 --snapshots-visible"), + examples.NewExample( + `Create a SFS resource pool with specific snapshot policy`, + "$ stackit beta sfs resource-pool create --availability-zone eu01-m --ip-acl 10.88.135.144/28 --performance-class Standard --size 500 --name resource-pool-01 --snapshot-policy-id XXX"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a resource-pool for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + resp, err := buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("create SFS resource pool: %w", err) + } + var resourcePoolId string + if resp != nil && resp.HasResourcePool() && resp.ResourcePool.HasId() { + resourcePoolId = *resp.ResourcePool.Id + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Create resource pool", func() error { + _, err = wait.CreateResourcePoolWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resourcePoolId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for resource pool creation: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int32(sizeFlag, 0, "Size of the pool in Gigabytes") + cmd.Flags().String(performanceClassFlag, "", "Performance class") + cmd.Flags().Var(flags.CIDRSliceFlag(), ipAclFlag, "List of network addresses in the form
, e.g. 192.168.10.0/24 that can mount the resource pool readonly") + cmd.Flags().String(availabilityZoneFlag, "", "Availability zone") + cmd.Flags().String(nameFlag, "", "Name") + cmd.Flags().Bool(snapshotsVisibleFlag, false, "Set snapshots visible and accessible to users") + cmd.Flags().String(snapshotPolicyIdFlag, "", "Set snapshot policy ID") + + for _, flag := range []string{sizeFlag, performanceClassFlag, ipAclFlag, availabilityZoneFlag, nameFlag} { + err := flags.MarkFlagsRequired(cmd, flag) + cobra.CheckErr(err) + } +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiCreateResourcePoolRequest { + req := apiClient.DefaultAPI.CreateResourcePool(ctx, model.ProjectId, model.Region) + req = req.CreateResourcePoolPayload(sfs.CreateResourcePoolPayload{ + AvailabilityZone: model.AvailabilityZone, + IpAcl: model.IpAcl, + Name: model.Name, + PerformanceClass: model.PerformanceClass, + SizeGigabytes: *model.SizeInGB, + SnapshotsAreVisible: &model.SnapshotsVisible, + SnapshotPolicyId: &model.SnapshotPolicyId, + }) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + performanceClass := flags.FlagToStringValue(p, cmd, performanceClassFlag) + size := flags.FlagToInt32Pointer(p, cmd, sizeFlag) + availabilityZone := flags.FlagToStringValue(p, cmd, availabilityZoneFlag) + ipAcls := flags.FlagToStringSliceValue(p, cmd, ipAclFlag) + name := flags.FlagToStringValue(p, cmd, nameFlag) + snapshotsVisible := flags.FlagToBoolValue(p, cmd, snapshotsVisibleFlag) + snapshotPolicyId := flags.FlagToStringValue(p, cmd, snapshotPolicyIdFlag) + + model := inputModel{ + GlobalFlagModel: globalFlags, + SizeInGB: size, + IpAcl: ipAcls, + PerformanceClass: performanceClass, + AvailabilityZone: availabilityZone, + Name: name, + SnapshotsVisible: snapshotsVisible, + SnapshotPolicyId: snapshotPolicyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, resp *sfs.CreateResourcePoolResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil || resp.ResourcePool == nil { + p.Outputln("Resource pool response is empty") + return nil + } + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + p.Outputf("%s resource pool for project %q. Resource pool ID: %s\n", operationState, projectLabel, utils.PtrString(resp.ResourcePool.Id)) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/resource-pool/create/create_test.go b/internal/cmd/beta/sfs/resource-pool/create/create_test.go new file mode 100644 index 000000000..53d47e36f --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/create/create_test.go @@ -0,0 +1,311 @@ +package create + +import ( + "context" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var ( + testProjectId = uuid.NewString() + testRegion = "eu02" + testResourcePoolPerformanceClass = "Standard" + testResourcePoolSizeInGB int32 = 50 + testResourcePoolAvailabilityZone = "eu02-m" + testResourcePoolName = "sfs-resource-pool-01" + testResourcePoolIpAcl = []string{"10.88.135.144/28", "250.81.87.224/32"} + testSnapshotsVisible = true + testSnapshotPolicyId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + performanceClassFlag: testResourcePoolPerformanceClass, + sizeFlag: strconv.FormatInt(int64(testResourcePoolSizeInGB), 10), + ipAclFlag: strings.Join(testResourcePoolIpAcl, ","), + availabilityZoneFlag: testResourcePoolAvailabilityZone, + nameFlag: testResourcePoolName, + snapshotsVisibleFlag: strconv.FormatBool(testSnapshotsVisible), + snapshotPolicyIdFlag: testSnapshotPolicyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + PerformanceClass: testResourcePoolPerformanceClass, + AvailabilityZone: testResourcePoolAvailabilityZone, + Name: testResourcePoolName, + SizeInGB: &testResourcePoolSizeInGB, + IpAcl: testResourcePoolIpAcl, + SnapshotsVisible: testSnapshotsVisible, + SnapshotPolicyId: testSnapshotPolicyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiCreateResourcePoolRequest)) sfs.ApiCreateResourcePoolRequest { + request := testClient.DefaultAPI.CreateResourcePool(testCtx, testProjectId, testRegion) + request = request.CreateResourcePoolPayload(sfs.CreateResourcePoolPayload{ + Name: testResourcePoolName, + PerformanceClass: testResourcePoolPerformanceClass, + AvailabilityZone: testResourcePoolAvailabilityZone, + IpAcl: testResourcePoolIpAcl, + SizeGigabytes: testResourcePoolSizeInGB, + SnapshotsAreVisible: &testSnapshotsVisible, + SnapshotPolicyId: &testSnapshotPolicyId, + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + ipAclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "ip acl missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipAclFlag) + }), + isValid: false, + }, + { + description: "name missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "performance class missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, performanceClassFlag) + }), + isValid: false, + }, + { + description: "size missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, sizeFlag) + }), + isValid: false, + }, + { + description: "availability zone missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, availabilityZoneFlag) + }), + isValid: false, + }, + { + description: "missing snapshot visible - fallback to false", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, snapshotsVisibleFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SnapshotsVisible = false + }), + isValid: true, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "repeated ip acl flags", + flagValues: fixtureFlagValues(), + ipAclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IpAcl = append(model.IpAcl, "198.51.100.14/24", "198.51.100.14/32") + }), + }, + { + description: "repeated ip acl flags with list value", + flagValues: fixtureFlagValues(), + ipAclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IpAcl = append(model.IpAcl, "198.51.100.14/24", "198.51.100.14/32") + }), + }, + { + description: "invalid ip acl 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipAclFlag] = "foo-bar" + }), + isValid: false, + }, + { + description: "invalid ip acl 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipAclFlag] = "192.168.178.256/32" + }), + isValid: false, + }, + { + description: "invalid ip acl 3", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipAclFlag] = "192.168.178.255/32," + }), + isValid: false, + }, + { + description: "invalid ip acl 4", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipAclFlag] = "192.168.178.255/32," + }), + isValid: false, + }, + { + description: "snapshot policy id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, snapshotPolicyIdFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SnapshotPolicyId = "" + }), + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + ipAclFlag: tt.ipAclValues, + }, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiCreateResourcePoolRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + projectLabel string + resp *sfs.CreateResourcePoolResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + resp: &sfs.CreateResourcePoolResponse{}, + }, + wantErr: false, + }, + { + name: "set response", + args: args{ + resp: &sfs.CreateResourcePoolResponse{ + ResourcePool: &sfs.ResourcePool{}, + }, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/resource-pool/delete/delete.go b/internal/cmd/beta/sfs/resource-pool/delete/delete.go new file mode 100644 index 000000000..d5cd7e1cd --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/delete/delete.go @@ -0,0 +1,123 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + resourcePoolIdArg = "RESOURCE_POOL_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a SFS resource pool", + Long: "Deletes a SFS resource pool.", + Args: args.SingleArg(resourcePoolIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete the SFS resource pool with ID "xxx"`, + "$ stackit beta sfs resource-pool delete xxx"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + resourcePoolName, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolName = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to delete resource pool %q? (This cannot be undone)", resourcePoolName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + resp, err := buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("delete SFS resource pool: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Delete resource pool", func() error { + _, err = wait.DeleteResourcePoolWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for resource pool deletion: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, resourcePoolName, resp) + }, + } + return cmd +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiDeleteResourcePoolRequest { + req := apiClient.DefaultAPI.DeleteResourcePool(ctx, model.ProjectId, model.Region, model.ResourcePoolId) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + resourcePoolId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourcePoolId: resourcePoolId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, async bool, resourcePoolName string, response map[string]interface{}) error { + return p.OutputResult(outputFormat, response, func() error { + operationState := "Deleted" + if async { + operationState = "Triggered deletion of" + } + p.Outputf("%s resource pool %q\n", operationState, resourcePoolName) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/resource-pool/delete/delete_test.go b/internal/cmd/beta/sfs/resource-pool/delete/delete_test.go new file mode 100644 index 000000000..1224548c0 --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/delete/delete_test.go @@ -0,0 +1,211 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testResourcePoolId = uuid.NewString() +var testRegion = "eu02" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testResourcePoolId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + ResourcePoolId: testResourcePoolId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiDeleteResourcePoolRequest)) sfs.ApiDeleteResourcePoolRequest { + request := testClient.DefaultAPI.DeleteResourcePool(testCtx, testProjectId, testRegion, testResourcePoolId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "resource pool id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "resource pool id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiDeleteResourcePoolRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + resourcePoolName string + response map[string]interface{} + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "empty - output json", + args: args{ + outputFormat: print.JSONOutputFormat, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.resourcePoolName, tt.args.response); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/resource-pool/describe/describe.go b/internal/cmd/beta/sfs/resource-pool/describe/describe.go new file mode 100644 index 000000000..72203f5e3 --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/describe/describe.go @@ -0,0 +1,162 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + resourcePoolIdArg = "RESOURCE_POOL_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Shows details of a SFS resource pool", + Long: "Shows details of a SFS resource pool.", + Args: args.SingleArg(resourcePoolIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe the SFS resource pool with ID "xxx"`, + "$ stackit beta sfs resource-pool describe xxx"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + resp, err := buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("describe SFS resource pool: %w", err) + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, model.ResourcePoolId, projectLabel, resp.ResourcePool) + }, + } + return cmd +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetResourcePoolRequest { + req := apiClient.DefaultAPI.GetResourcePool(ctx, model.ProjectId, model.Region, model.ResourcePoolId) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + resourcePoolId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourcePoolId: resourcePoolId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, resourcePoolId, projectLabel string, resourcePool *sfs.ResourcePool) error { + return p.OutputResult(outputFormat, resourcePool, func() error { + if resourcePool == nil { + p.Outputf("Resource pool %q not found in project %q\n", resourcePoolId, projectLabel) + return nil + } + table := tables.NewTable() + + // convert the string slice to a comma separated list + var ipAclStr string + if resourcePool.IpAcl != nil { + ipAclStr = strings.Join(resourcePool.IpAcl, ", ") + } + + var snapshotPolicyId string + if resourcePool.SnapshotPolicy.Get() != nil { + snapshotPolicyId = *resourcePool.SnapshotPolicy.Get().Id + } + + table.AddRow("ID", utils.PtrString(resourcePool.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(resourcePool.Name)) + table.AddSeparator() + table.AddRow("AVAILABILITY ZONE", utils.PtrString(resourcePool.AvailabilityZone)) + table.AddSeparator() + table.AddRow("NUMBER OF SHARES", utils.PtrString(resourcePool.CountShares)) + table.AddSeparator() + table.AddRow("IP ACL", ipAclStr) + table.AddSeparator() + table.AddRow("MOUNT PATH", utils.PtrString(resourcePool.MountPath)) + table.AddSeparator() + if resourcePool.PerformanceClass != nil { + table.AddRow("PERFORMANCE CLASS", utils.PtrString(resourcePool.PerformanceClass.Name)) + table.AddSeparator() + } + table.AddRow("SNAPSHOTS ARE VISIBLE", utils.PtrString(resourcePool.SnapshotsAreVisible)) + table.AddSeparator() + table.AddRow("SNAPSHOT POLICY ID", snapshotPolicyId) + table.AddSeparator() + table.AddRow("NEXT PERFORMANCE CLASS DOWNGRADE TIME", resourcePool.PerformanceClassDowngradableAt) + table.AddSeparator() + table.AddRow("NEXT SIZE REDUCTION TIME", resourcePool.SizeReducibleAt) + table.AddSeparator() + if resourcePool.HasSpace() { + table.AddRow("TOTAL SIZE (GB)", utils.PtrString(resourcePool.Space.SizeGigabytes)) + table.AddSeparator() + table.AddRow("AVAILABLE SIZE (GB)", utils.PtrString(resourcePool.Space.AvailableGigabytes.Get())) + table.AddSeparator() + table.AddRow("USED SIZE (GB)", utils.PtrString(resourcePool.Space.UsedGigabytes.Get())) + table.AddSeparator() + table.AddRow("USED BY SNAPSHOTS (GB)", utils.PtrString(resourcePool.Space.UsedBySnapshotsGigabytes.Get())) + table.AddSeparator() + } + table.AddRow("STATE", utils.PtrString(resourcePool.State)) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/sfs/resource-pool/describe/describe_test.go b/internal/cmd/beta/sfs/resource-pool/describe/describe_test.go new file mode 100644 index 000000000..36f27fa7c --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/describe/describe_test.go @@ -0,0 +1,231 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testResourcePoolId = uuid.NewString() +var testRegion = "eu02" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testResourcePoolId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + ResourcePoolId: testResourcePoolId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetResourcePoolRequest)) sfs.ApiGetResourcePoolRequest { + request := testClient.DefaultAPI.GetResourcePool(testCtx, testProjectId, testRegion, testResourcePoolId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "resource pool id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "resource pool id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetResourcePoolRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resourcePoolId string + projectLabel string + resp *sfs.ResourcePool + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + resp: &sfs.ResourcePool{}, + }, + wantErr: false, + }, + { + name: "full response", + args: args{ + resp: &sfs.ResourcePool{ + Id: utils.Ptr("id"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("az"), + State: utils.Ptr("state"), + Space: &sfs.ResourcePoolSpace{ + SizeGigabytes: utils.Ptr(int32(100)), + AvailableGigabytes: *sfs.NewNullableFloat64(utils.Ptr(float64(50))), + UsedGigabytes: *sfs.NewNullableFloat64(utils.Ptr(float64(50))), + UsedBySnapshotsGigabytes: *sfs.NewNullableFloat64(utils.Ptr(float64(10))), + }, + }, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resourcePoolId, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/resource-pool/list/list.go b/internal/cmd/beta/sfs/resource-pool/list/list.go new file mode 100644 index 000000000..aae15972b --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/list/list.go @@ -0,0 +1,156 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all SFS resource pools", + Long: "Lists all SFS resource pools.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all SFS resource pools`, + "$ stackit beta sfs resource-pool list"), + examples.NewExample( + `List all SFS resource pools for another region than the default one`, + "$ stackit beta sfs resource-pool list --region eu01"), + examples.NewExample( + `List up to 10 SFS resource pools`, + "$ stackit beta sfs resource-pool list --limit 10"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + resp, err := buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("list SFS resource pools: %w", err) + } + + resourcePools := utils.GetSliceFromPointer(&resp.ResourcePools) + + // Truncate output + if model.Limit != nil && len(resourcePools) > int(*model.Limit) { + resourcePools = resourcePools[:*model.Limit] + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resourcePools) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiListResourcePoolsRequest { + req := apiClient.DefaultAPI.ListResourcePools(ctx, model.ProjectId, model.Region) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &cliErr.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} +func outputResult(p *print.Printer, outputFormat, projectLabel string, resourcePools []sfs.ResourcePool) error { + return p.OutputResult(outputFormat, resourcePools, func() error { + if len(resourcePools) == 0 { + p.Outputf("No resource pools found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "AVAILABILITY ZONE", "STATE", "TOTAL SIZE (GB)", "USED SIZE (GB)", "USED BY SNAPSHOTS (GB)") + for _, resourcePool := range resourcePools { + totalSizeGigabytes, usedSizeGigabytes, usedBySnapshotsGigabytes := "", "", "" + if resourcePool.HasSpace() { + totalSizeGigabytes = utils.PtrString(resourcePool.Space.SizeGigabytes) + usedSizeGigabytes = utils.PtrString(resourcePool.Space.UsedGigabytes.Get()) + usedBySnapshotsGigabytes = utils.PtrString(resourcePool.Space.UsedBySnapshotsGigabytes.Get()) + } + table.AddRow( + utils.PtrString(resourcePool.Id), + utils.PtrString(resourcePool.Name), + utils.PtrString(resourcePool.AvailabilityZone), + utils.PtrString(resourcePool.State), + totalSizeGigabytes, + usedSizeGigabytes, + usedBySnapshotsGigabytes, + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/beta/sfs/resource-pool/list/list_test.go b/internal/cmd/beta/sfs/resource-pool/list/list_test.go new file mode 100644 index 000000000..a7f2a2d32 --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/list/list_test.go @@ -0,0 +1,219 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testRegion = "eu02" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListResourcePoolsRequest)) sfs.ApiListResourcePoolsRequest { + request := testClient.DefaultAPI.ListResourcePools(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "limit invalid 3", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-5" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiListResourcePoolsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resourcePools []sfs.ResourcePool + projectLabel string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty resource pools slice", + args: args{ + resourcePools: []sfs.ResourcePool{}, + }, + wantErr: false, + }, + { + name: "set empty resource pool in resource pools slice", + args: args{ + resourcePools: []sfs.ResourcePool{{}}, + }, + wantErr: false, + }, + { + name: "values", + args: args{ + resourcePools: []sfs.ResourcePool{ + { + Id: utils.Ptr("id"), + Name: utils.Ptr("name"), + AvailabilityZone: utils.Ptr("az"), + State: utils.Ptr("state"), + Space: &sfs.ResourcePoolSpace{ + SizeGigabytes: utils.Ptr(int32(100)), + AvailableGigabytes: *sfs.NewNullableFloat64(utils.Ptr(float64(50))), + UsedGigabytes: *sfs.NewNullableFloat64(utils.Ptr(float64(50))), + UsedBySnapshotsGigabytes: *sfs.NewNullableFloat64(utils.Ptr(float64(10))), + }, + }, + }, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.resourcePools); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/resource-pool/resource_pool.go b/internal/cmd/beta/sfs/resource-pool/resource_pool.go new file mode 100644 index 000000000..3198741b4 --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/resource_pool.go @@ -0,0 +1,34 @@ +package resourcepool + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "resource-pool", + Short: "Provides functionality for SFS resource pools", + Long: "Provides functionality for SFS resource pools.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/resource-pool/update/update.go b/internal/cmd/beta/sfs/resource-pool/update/update.go new file mode 100644 index 000000000..227f79097 --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/update/update.go @@ -0,0 +1,191 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + resourcePoolIdArg = "RESOURCE_POOL_ID" + performanceClassFlag = "performance-class" + sizeFlag = "size" + ipAclFlag = "ip-acl" + snapshotsVisibleFlag = "snapshots-visible" + snapshotPolicyIdFlag = "snapshot-policy-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + SizeGigabytes *int32 + PerformanceClass *string + IpAcl []string + ResourcePoolId string + SnapshotsVisible *bool + SnapshotPolicyId *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates a SFS resource pool", + Long: `Updates a SFS resource pool. + +The available performance class values can be obtained by running: + $ stackit beta sfs performance-class list`, + Args: args.SingleArg(resourcePoolIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the SFS resource pool with ID "xxx"`, + "$ stackit beta sfs resource-pool update xxx --ip-acl 10.88.135.144/28 --performance-class Standard --size 5"), + examples.NewExample( + `Update the SFS resource pool with ID "xxx", allow only a single IP which can mount the resource pool`, + "$ stackit beta sfs resource-pool update xxx --ip-acl 250.81.87.224/32 --performance-class Standard --size 5"), + examples.NewExample( + `Update the SFS resource pool with ID "xxx", allow multiple IP ACL which can mount the resource pool`, + "$ stackit beta sfs resource-pool update xxx --ip-acl \"10.88.135.144/28,250.81.87.224/32\" --performance-class Standard --size 5"), + examples.NewExample( + `Update the SFS resource pool with ID "xxx", set snapshots visible to false`, + "$ stackit beta sfs resource-pool update xxx --snapshots-visible=false"), + examples.NewExample( + `Update the SFS resource pool with ID "xxx" to set snapshot policy id to "YYY"`, + "$ stackit beta sfs resource-pool update xxx --snapshot-policy-id YYY"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + resourcePoolName, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolName = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to update resource-pool %q for project %q?", resourcePoolName, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + resp, err := buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("update SFS resource pool: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Update resource pool", func() error { + _, err = wait.UpdateResourcePoolWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for resource pool update: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int32(sizeFlag, 0, "Size of the pool in Gigabytes") + cmd.Flags().String(performanceClassFlag, "", "Performance class") + cmd.Flags().Var(flags.CIDRSliceFlag(), ipAclFlag, "List of network addresses in the form
, e.g. 192.168.10.0/24 that can mount the resource pool readonly") + cmd.Flags().Bool(snapshotsVisibleFlag, false, "Set snapshots visible and accessible to users") + cmd.Flags().String(snapshotPolicyIdFlag, "", "Set snapshot policy ID") +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiUpdateResourcePoolRequest { + req := apiClient.DefaultAPI.UpdateResourcePool(ctx, model.ProjectId, model.Region, model.ResourcePoolId) + req = req.UpdateResourcePoolPayload(sfs.UpdateResourcePoolPayload{ + IpAcl: model.IpAcl, + PerformanceClass: model.PerformanceClass, + SizeGigabytes: *sfs.NewNullableInt32(model.SizeGigabytes), + SnapshotsAreVisible: model.SnapshotsVisible, + SnapshotPolicyId: model.SnapshotPolicyId, + }) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + resourcePoolId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + performanceClass := flags.FlagToStringPointer(p, cmd, performanceClassFlag) + size := flags.FlagToInt32Pointer(p, cmd, sizeFlag) + ipAcls := flags.FlagToStringSliceValue(p, cmd, ipAclFlag) + snapshotsVisible := flags.FlagToBoolPointer(p, cmd, snapshotsVisibleFlag) + snapshotPolicyId := flags.FlagToStringPointer(p, cmd, snapshotPolicyIdFlag) + + if performanceClass == nil && size == nil && ipAcls == nil && snapshotsVisible == nil && snapshotPolicyId == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SizeGigabytes: size, + IpAcl: ipAcls, + PerformanceClass: performanceClass, + ResourcePoolId: resourcePoolId, + SnapshotsVisible: snapshotsVisible, + SnapshotPolicyId: snapshotPolicyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, async bool, resp *sfs.UpdateResourcePoolResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil || resp.ResourcePool == nil { + p.Outputln("Resource pool response is empty") + return nil + } + operationState := "Updated" + if async { + operationState = "Triggered update of" + } + p.Outputf("%s resource pool %s\n", operationState, utils.PtrString(resp.ResourcePool.Name)) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/resource-pool/update/update_test.go b/internal/cmd/beta/sfs/resource-pool/update/update_test.go new file mode 100644 index 000000000..a5cd7e75e --- /dev/null +++ b/internal/cmd/beta/sfs/resource-pool/update/update_test.go @@ -0,0 +1,379 @@ +package update + +import ( + "context" + "slices" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu02" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testResourcePoolId = uuid.NewString() + testResourcePoolIpAcl = []string{"10.88.135.144/28", "250.81.87.224/32"} + testResourcePoolPerformanceClass = "Standard" + testResourcePoolSizeInGB int32 = 50 + testSnapshotsVisible = true + testSnapshotPolicyId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testResourcePoolId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + performanceClassFlag: testResourcePoolPerformanceClass, + sizeFlag: strconv.FormatInt(int64(testResourcePoolSizeInGB), 10), + ipAclFlag: strings.Join(testResourcePoolIpAcl, ","), + snapshotsVisibleFlag: strconv.FormatBool(testSnapshotsVisible), + snapshotPolicyIdFlag: testSnapshotPolicyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + ipAclClone := slices.Clone(testResourcePoolIpAcl) + + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + ResourcePoolId: testResourcePoolId, + SizeGigabytes: &testResourcePoolSizeInGB, + PerformanceClass: &testResourcePoolPerformanceClass, + IpAcl: ipAclClone, + SnapshotsVisible: &testSnapshotsVisible, + SnapshotPolicyId: &testSnapshotPolicyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiUpdateResourcePoolRequest)) sfs.ApiUpdateResourcePoolRequest { + request := testClient.DefaultAPI.UpdateResourcePool(testCtx, testProjectId, testRegion, testResourcePoolId) + request = request.UpdateResourcePoolPayload(sfs.UpdateResourcePoolPayload{ + IpAcl: testResourcePoolIpAcl, + PerformanceClass: &testResourcePoolPerformanceClass, + SizeGigabytes: *sfs.NewNullableInt32(&testResourcePoolSizeInGB), + SnapshotsAreVisible: &testSnapshotsVisible, + SnapshotPolicyId: &testSnapshotPolicyId, + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + ipAclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no values to update", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, sizeFlag) + delete(flagValues, ipAclFlag) + delete(flagValues, performanceClassFlag) + delete(flagValues, snapshotsVisibleFlag) + delete(flagValues, snapshotPolicyIdFlag) + }), + isValid: false, + }, + { + description: "update only size", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipAclFlag) + delete(flagValues, performanceClassFlag) + delete(flagValues, snapshotsVisibleFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IpAcl = nil + model.PerformanceClass = nil + model.SnapshotsVisible = nil + }), + isValid: true, + }, + { + description: "update only snapshots visibility", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipAclFlag) + delete(flagValues, performanceClassFlag) + delete(flagValues, sizeFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IpAcl = nil + model.PerformanceClass = nil + model.SizeGigabytes = nil + }), + isValid: true, + }, + { + description: "update only performance class", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipAclFlag) + delete(flagValues, snapshotsVisibleFlag) + delete(flagValues, sizeFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IpAcl = nil + model.SnapshotsVisible = nil + model.SizeGigabytes = nil + }), + isValid: true, + }, + { + description: "update only ipAcl", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, performanceClassFlag) + delete(flagValues, snapshotsVisibleFlag) + delete(flagValues, sizeFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.PerformanceClass = nil + model.SnapshotsVisible = nil + model.SizeGigabytes = nil + }), + isValid: true, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + flagValues[sizeFlag] = "50" + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + flagValues[sizeFlag] = "50" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + flagValues[sizeFlag] = "50" + }), + isValid: false, + }, + { + description: "resource pool id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "resource pool id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "repeated acl flags", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + ipAclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + if model.IpAcl == nil { + model.IpAcl = []string{} + } + model.IpAcl = append(model.IpAcl, "198.51.100.14/24", "198.51.100.14/32") + }), + }, + { + description: "repeated ip acl flag with list value", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + ipAclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + if model.IpAcl == nil { + model.IpAcl = []string{} + } + model.IpAcl = append(model.IpAcl, "198.51.100.14/24", "198.51.100.14/32") + }), + }, + { + description: "snapshot policy id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, snapshotPolicyIdFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SnapshotPolicyId = nil + }), + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + ipAclFlag: tt.ipAclValues, + }, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiUpdateResourcePoolRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(sfs.NullableInt32{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + resp *sfs.UpdateResourcePoolResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "empty response", + args: args{ + resp: &sfs.UpdateResourcePoolResponse{}, + }, + wantErr: false, + }, + { + name: "valid response with empty resource pool", + args: args{ + resp: &sfs.UpdateResourcePoolResponse{ + ResourcePool: &sfs.ResourcePool{}, + }, + }, + wantErr: false, + }, + { + name: "valid response with name", + args: args{ + resp: &sfs.UpdateResourcePoolResponse{ + ResourcePool: &sfs.ResourcePool{ + Name: utils.Ptr("example name"), + }, + }, + }, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/sfs.go b/internal/cmd/beta/sfs/sfs.go new file mode 100644 index 000000000..ed4492fef --- /dev/null +++ b/internal/cmd/beta/sfs/sfs.go @@ -0,0 +1,38 @@ +package sfs + +import ( + exportpolicy "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/export-policy" + performanceclass "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/performance-class" + projectlock "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/project-lock" + resourcepool "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/resource-pool" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot" + snapshotpolicy "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot-policy" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "sfs", + Short: "Provides functionality for SFS (STACKIT File Storage)", + Long: "Provides functionality for SFS (STACKIT File Storage).", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(resourcepool.NewCmd(params)) + cmd.AddCommand(share.NewCmd(params)) + cmd.AddCommand(exportpolicy.NewCmd(params)) + cmd.AddCommand(snapshot.NewCmd(params)) + cmd.AddCommand(snapshotpolicy.NewCmd(params)) + cmd.AddCommand(performanceclass.NewCmd(params)) + cmd.AddCommand(projectlock.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/share/create/create.go b/internal/cmd/beta/sfs/share/create/create.go new file mode 100644 index 000000000..5d57a5d7d --- /dev/null +++ b/internal/cmd/beta/sfs/share/create/create.go @@ -0,0 +1,180 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + nameFlag = "name" + resourcePoolIdFlag = "resource-pool-id" + exportPolicyNameFlag = "export-policy-name" + hardLimitFlag = "hard-limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Name string + ExportPolicyName *string + ResourcePoolId string + HardLimit *int32 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a share", + Long: "Creates a share.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a share in a resource pool with ID "xxx", name "yyy" and no space hard limit`, + "$ stackit beta sfs share create --resource-pool-id xxx --name yyy --hard-limit 0", + ), + examples.NewExample( + `Create a share in a resource pool with ID "xxx", name "yyy" and export policy with name "zzz"`, + "$ stackit beta sfs share create --resource-pool-id xxx --name yyy --export-policy-name zzz --hard-limit 0", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to create a SFS share for resource pool %q?", resourcePoolLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create SFS share: %w", err) + } + var shareId string + if resp != nil && resp.HasShare() && resp.Share.HasId() { + shareId = *resp.Share.Id + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating share", func() error { + _, err = wait.CreateShareWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId, shareId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("waiting for share creation: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, resourcePoolLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Share name") + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool the share is assigned to") + cmd.Flags().String(exportPolicyNameFlag, "", "The export policy the share is assigned to") + cmd.Flags().Int32(hardLimitFlag, 0, "The space hard limit for the share") + + err := flags.MarkFlagsRequired(cmd, nameFlag, resourcePoolIdFlag, hardLimitFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + hardLimit := flags.FlagToInt32Pointer(p, cmd, hardLimitFlag) + if hardLimit != nil { + if *hardLimit < 0 { + return nil, &errors.FlagValidationError{ + Flag: hardLimitFlag, + Details: "must be a positive integer", + } + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + ExportPolicyName: flags.FlagToStringPointer(p, cmd, exportPolicyNameFlag), + HardLimit: hardLimit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiCreateShareRequest { + req := apiClient.DefaultAPI.CreateShare(ctx, model.ProjectId, model.Region, model.ResourcePoolId) + req = req.CreateSharePayload( + sfs.CreateSharePayload{ + Name: model.Name, + ExportPolicyName: *sfs.NewNullableString(model.ExportPolicyName), + SpaceHardLimitGigabytes: *model.HardLimit, + }, + ) + return req +} + +func outputResult(p *print.Printer, outputFormat string, async bool, resourcePoolLabel string, item *sfs.CreateShareResponse) error { + return p.OutputResult(outputFormat, item, func() error { + if item == nil || item.Share == nil { + p.Outputln("SFS share response is empty") + return nil + } + operation := "Created" + if async { + operation = "Triggered creation of" + } + p.Outputf( + "%s SFS Share %q in resource pool %q.\nShare ID: %s\n", + operation, + utils.PtrString(item.Share.Name), + resourcePoolLabel, + utils.PtrString(item.Share.Id), + ) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/share/create/create_test.go b/internal/cmd/beta/sfs/share/create/create_test.go new file mode 100644 index 000000000..9727c0d0a --- /dev/null +++ b/internal/cmd/beta/sfs/share/create/create_test.go @@ -0,0 +1,207 @@ +package create + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testName = "test-name" +var testResourcePoolId = uuid.NewString() +var testExportPolicyName = "test-export-policy" +var testHardLimit int32 = 10 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + nameFlag: testName, + resourcePoolIdFlag: testResourcePoolId, + exportPolicyNameFlag: testExportPolicyName, + hardLimitFlag: strconv.Itoa(int(testHardLimit)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: testName, + ResourcePoolId: testResourcePoolId, + ExportPolicyName: utils.Ptr(testExportPolicyName), + HardLimit: utils.Ptr(testHardLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiCreateShareRequest)) sfs.ApiCreateShareRequest { + request := testClient.DefaultAPI.CreateShare(testCtx, testProjectId, testRegion, testResourcePoolId) + request = request.CreateSharePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(request *sfs.CreateSharePayload)) sfs.CreateSharePayload { + payload := sfs.CreateSharePayload{ + Name: testName, + ExportPolicyName: *sfs.NewNullableString(utils.Ptr(testExportPolicyName)), + SpaceHardLimitGigabytes: testHardLimit, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "required only", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, exportPolicyNameFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ExportPolicyName = nil + }), + }, + { + description: "missing required name", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "missing required resourcePoolId", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiCreateShareRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(sfs.NullableString{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + resourcePoolLabel string + item *sfs.CreateShareResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + item: &sfs.CreateShareResponse{}, + }, + wantErr: false, + }, + { + name: "set empty response share", + args: args{ + item: &sfs.CreateShareResponse{ + Share: &sfs.Share{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.resourcePoolLabel, tt.args.item); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/share/delete/delete.go b/internal/cmd/beta/sfs/share/delete/delete.go new file mode 100644 index 000000000..ed4cf417b --- /dev/null +++ b/internal/cmd/beta/sfs/share/delete/delete.go @@ -0,0 +1,133 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + shareIdArg = "SHARE_ID" + + resourcePoolIdFlag = "resource-pool-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + ShareId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", shareIdArg), + Short: "Deletes a share", + Long: "Deletes a share.", + Args: args.SingleArg(shareIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a share with ID "xxx" from a resource pool with ID "yyy"`, + "$ stackit beta sfs share delete xxx --resource-pool-id yyy", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + shareLabel, err := sfsUtils.GetShareName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get share name: %v", err) + shareLabel = model.ShareId + } else if shareLabel == "" { + shareLabel = model.ShareId + } + + prompt := fmt.Sprintf("Are you sure you want to delete SFS share %q? (This cannot be undone)", shareLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete SFS share: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting share", func() error { + _, err = wait.DeleteShareWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("waiting for share deletion: %w", err) + } + } + + operation := "Deleted" + if model.Async { + operation = "Triggered deletion of" + } + + params.Printer.Outputf("%s share %q\n", operation, shareLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool the share is assigned to") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + shareId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ShareId: shareId, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiDeleteShareRequest { + return apiClient.DefaultAPI.DeleteShare(ctx, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId) +} diff --git a/internal/cmd/beta/sfs/share/delete/delete_test.go b/internal/cmd/beta/sfs/share/delete/delete_test.go new file mode 100644 index 000000000..e8c63f367 --- /dev/null +++ b/internal/cmd/beta/sfs/share/delete/delete_test.go @@ -0,0 +1,187 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() +var testShareId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testShareId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + ShareId: testShareId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiDeleteShareRequest)) sfs.ApiDeleteShareRequest { + request := testClient.DefaultAPI.DeleteShare(testCtx, testProjectId, testRegion, testResourcePoolId, testShareId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "share id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "share id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing required resourcePoolId", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiDeleteShareRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/share/describe/describe.go b/internal/cmd/beta/sfs/share/describe/describe.go new file mode 100644 index 000000000..e2756f757 --- /dev/null +++ b/internal/cmd/beta/sfs/share/describe/describe.go @@ -0,0 +1,192 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + shareIdArg = "SHARE_ID" + + resourcePoolIdFlag = "resource-pool-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + ShareId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", shareIdArg), + Short: "Shows details of a shares", + Long: "Shows details of a shares.", + Args: args.SingleArg(shareIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a shares with ID "xxx" from resource pool with ID "yyy"`, + "$ stackit beta sfs share describe xxx --resource-pool-id yyy", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe SFS share: %w", err) + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + return outputResult(params.Printer, model.OutputFormat, resourcePoolLabel, model.ShareId, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool the share is assigned to") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + shareId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + ShareId: shareId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetShareRequest { + return apiClient.DefaultAPI.GetShare(ctx, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId) +} + +func outputResult(p *print.Printer, outputFormat, resourcePoolLabel, shareId string, share *sfs.GetShareResponse) error { + return p.OutputResult(outputFormat, share, func() error { + if share == nil || share.Share == nil { + p.Outputf("Share %q not found in resource pool %q\n", shareId, resourcePoolLabel) + return nil + } + + var content []tables.Table + + table := tables.NewTable() + table.SetTitle("Share") + item := *share.Share + + table.AddRow("ID", utils.PtrString(item.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(item.Name)) + table.AddSeparator() + table.AddRow("STATE", utils.PtrString(item.State)) + table.AddSeparator() + table.AddRow("MOUNT PATH", utils.PtrString(item.MountPath)) + table.AddSeparator() + table.AddRow("HARD LIMIT (GB)", utils.PtrString(item.SpaceHardLimitGigabytes)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(item.CreatedAt)) + + content = append(content, table) + + if item.HasExportPolicy() { + policyTable := tables.NewTable() + policyTable.SetTitle("Export Policy") + + policyTable.SetHeader( + "ID", + "NAME", + "SHARES USING EXPORT POLICY", + "CREATED AT", + ) + + policy := item.ExportPolicy.Get() + + policyTable.AddRow( + utils.PtrString(policy.Id), + utils.PtrString(policy.Name), + utils.PtrString(policy.SharesUsingExportPolicy), + utils.ConvertTimePToDateTimeString(policy.CreatedAt), + ) + + content = append(content, policyTable) + + if len(policy.Rules) > 0 { + ruleTable := tables.NewTable() + ruleTable.SetTitle("Export Policy - Rules") + + ruleTable.SetHeader("ID", "ORDER", "DESCRIPTION", "IP ACL", "READ ONLY", "SET UUID", "SUPER USER", "CREATED AT") + + for _, rule := range policy.Rules { + var description string + if rule.Description.IsSet() && *rule.Description.Get() != "" { + description = utils.PtrString(rule.Description.Get()) + } + ruleTable.AddRow( + utils.PtrString(rule.Id), + utils.PtrString(rule.Order), + description, + utils.JoinStringPtr(&rule.IpAcl, ", "), + utils.PtrString(rule.ReadOnly), + utils.PtrString(rule.SetUuid), + utils.PtrString(rule.SuperUser), + utils.ConvertTimePToDateTimeString(rule.CreatedAt), + ) + ruleTable.AddSeparator() + } + + content = append(content, ruleTable) + } + } + + if err := tables.DisplayTables(p, content); err != nil { + return fmt.Errorf("render tables: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/sfs/share/describe/describe_test.go b/internal/cmd/beta/sfs/share/describe/describe_test.go new file mode 100644 index 000000000..6848ae685 --- /dev/null +++ b/internal/cmd/beta/sfs/share/describe/describe_test.go @@ -0,0 +1,233 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() +var testShareId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testShareId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + ShareId: testShareId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetShareRequest)) sfs.ApiGetShareRequest { + request := testClient.DefaultAPI.GetShare(testCtx, testProjectId, testRegion, testResourcePoolId, testShareId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "share id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "share id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing required resourcePoolId", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetShareRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + shareId string + resourcePoolLabel string + share *sfs.GetShareResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + share: &sfs.GetShareResponse{}, + }, + wantErr: false, + }, + { + name: "set empty share", + args: args{ + share: &sfs.GetShareResponse{ + Share: &sfs.Share{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resourcePoolLabel, tt.args.shareId, tt.args.share); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/share/list/list.go b/internal/cmd/beta/sfs/share/list/list.go new file mode 100644 index 000000000..5816a9185 --- /dev/null +++ b/internal/cmd/beta/sfs/share/list/list.go @@ -0,0 +1,159 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + resourcePoolIdFlag = "resource-pool-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all shares of a resource pool", + Long: "Lists all shares of a resource pool.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all shares from resource pool with ID "xxx"`, + "$ stackit beta sfs share list --resource-pool-id xxx", + ), + examples.NewExample( + `List up to 10 shares from resource pool with ID "xxx"`, + "$ stackit beta sfs share list --resource-pool-id xxx --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list SFS share: %w", err) + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.Shares) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, resourcePoolLabel, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool the share is assigned to") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be grater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiListSharesRequest { + return apiClient.DefaultAPI.ListShares(ctx, model.ProjectId, model.Region, model.ResourcePoolId) +} + +func outputResult(p *print.Printer, outputFormat, resourcePoolLabel string, shares []sfs.Share) error { + return p.OutputResult(outputFormat, shares, func() error { + if len(shares) == 0 { + p.Info("No shares found for resource pool %q\n", resourcePoolLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "STATE", "EXPORT POLICY", "MOUNT PATH", "HARD LIMIT (GB)", "CREATED AT") + + for _, share := range shares { + var policy string + if share.ExportPolicy.IsSet() && (share.ExportPolicy.Get().GetId() != "" || share.ExportPolicy.Get().GetName() != "") { + if name, ok := share.ExportPolicy.Get().GetNameOk(); ok { + policy = *name + } else if id, ok := share.ExportPolicy.Get().GetIdOk(); ok { + policy = *id + } + } + table.AddRow( + utils.PtrString(share.Id), + utils.PtrString(share.Name), + utils.PtrString(share.State), + policy, + utils.PtrString(share.MountPath), + utils.PtrString(share.SpaceHardLimitGigabytes), + utils.ConvertTimePToDateTimeString(share.CreatedAt), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/share/list/list_test.go b/internal/cmd/beta/sfs/share/list/list_test.go new file mode 100644 index 000000000..acf1d8b04 --- /dev/null +++ b/internal/cmd/beta/sfs/share/list/list_test.go @@ -0,0 +1,207 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() +var testLimit int64 = 10 + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + limitFlag: strconv.FormatInt(testLimit, 10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + Limit: utils.Ptr(testLimit), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListSharesRequest)) sfs.ApiListSharesRequest { + request := testClient.DefaultAPI.ListShares(testCtx, testProjectId, testRegion, testResourcePoolId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing required resourcePoolId", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + { + description: "invalid limit 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + { + description: "invalid limit 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-1" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiListSharesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resourcePoolLabel string + shares []sfs.Share + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty share in shares", + args: args{ + shares: []sfs.Share{{}}, + }, + wantErr: false, + }, + { + name: "set empty shares", + args: args{ + shares: []sfs.Share{}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resourcePoolLabel, tt.args.shares); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/share/share.go b/internal/cmd/beta/sfs/share/share.go new file mode 100644 index 000000000..1ea180fd2 --- /dev/null +++ b/internal/cmd/beta/sfs/share/share.go @@ -0,0 +1,34 @@ +package share + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/share/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "share", + Short: "Provides functionality for SFS shares", + Long: "Provides functionality for SFS shares.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/share/update/update.go b/internal/cmd/beta/sfs/share/update/update.go new file mode 100644 index 000000000..5e6112f4f --- /dev/null +++ b/internal/cmd/beta/sfs/share/update/update.go @@ -0,0 +1,180 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + shareIdArg = "SHARE_ID" + + resourcePoolIdFlag = "resource-pool-id" + exportPolicyNameFlag = "export-policy-name" + hardLimitFlag = "hard-limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ShareId string + ResourcePoolId string + ExportPolicyName *string + HardLimit *int32 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", shareIdArg), + Short: "Updates a share", + Long: "Updates a share.", + Args: args.SingleArg(shareIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update share with ID "xxx" with new export-policy-name "yyy" in resource-pool "zzz"`, + "$ stackit beta sfs share update xxx --export-policy-name yyy --resource-pool-id zzz", + ), + examples.NewExample( + `Update share with ID "xxx" with new space hard limit "50" in resource-pool "yyy"`, + "$ stackit beta sfs share update xxx --hard-limit 50 --resource-pool-id yyy", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + shareLabel, err := sfsUtils.GetShareName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get share name: %v", err) + shareLabel = model.ShareId + } else if shareLabel == "" { + shareLabel = model.ShareId + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to update SFS share %q for resource pool %q?", shareLabel, resourcePoolLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update SFS share: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Updating share", func() error { + _, err = wait.UpdateShareWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("waiting for share update: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, resourcePoolLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool the share is assigned to") + cmd.Flags().String(exportPolicyNameFlag, "", "The export policy the share is assigned to") + cmd.Flags().Int32(hardLimitFlag, 0, "The space hard limit for the share") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + shareId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + hardLimit := flags.FlagToInt32Pointer(p, cmd, hardLimitFlag) + if hardLimit != nil && *hardLimit < 0 { + return nil, &errors.FlagValidationError{ + Flag: hardLimitFlag, + Details: "must be a positive integer", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + ExportPolicyName: flags.FlagToStringPointer(p, cmd, exportPolicyNameFlag), + HardLimit: hardLimit, + ShareId: shareId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiUpdateShareRequest { + req := apiClient.DefaultAPI.UpdateShare(ctx, model.ProjectId, model.Region, model.ResourcePoolId, model.ShareId) + req = req.UpdateSharePayload(sfs.UpdateSharePayload{ + ExportPolicyName: *sfs.NewNullableString(model.ExportPolicyName), + SpaceHardLimitGigabytes: *sfs.NewNullableInt32(model.HardLimit), + }) + return req +} + +func outputResult(p *print.Printer, outputFormat string, async bool, resourcePoolLabel string, item *sfs.UpdateShareResponse) error { + return p.OutputResult(outputFormat, item, func() error { + if item == nil || item.Share == nil { + p.Outputln("SFS share response is empty") + return nil + } + + operation := "Updated" + if async { + operation = "Triggered update of" + } + p.Outputf( + "%s SFS share %q in resource pool %q.\n", + operation, + utils.PtrString(item.Share.Name), + resourcePoolLabel, + ) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/share/update/update_test.go b/internal/cmd/beta/sfs/share/update/update_test.go new file mode 100644 index 000000000..1184bdbd8 --- /dev/null +++ b/internal/cmd/beta/sfs/share/update/update_test.go @@ -0,0 +1,266 @@ +package update + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() +var testShareId = uuid.NewString() +var testHardLimit int32 = 10 +var testExportPolicy = "test-export-policy" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + hardLimitFlag: strconv.FormatInt(int64(testHardLimit), 10), + exportPolicyNameFlag: testExportPolicy, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testShareId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + ShareId: testShareId, + HardLimit: utils.Ptr(testHardLimit), + ExportPolicyName: utils.Ptr(testExportPolicy), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiUpdateShareRequest)) sfs.ApiUpdateShareRequest { + request := testClient.DefaultAPI.UpdateShare(testCtx, testProjectId, testRegion, testResourcePoolId, testShareId) + request = request.UpdateSharePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *sfs.UpdateSharePayload)) sfs.UpdateSharePayload { + payload := sfs.UpdateSharePayload{ + ExportPolicyName: *sfs.NewNullableString(utils.Ptr(testExportPolicy)), + SpaceHardLimitGigabytes: *sfs.NewNullableInt32(utils.Ptr(testHardLimit)), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "only required flags", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, exportPolicyNameFlag) + delete(flagValues, hardLimitFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ExportPolicyName = nil + model.HardLimit = nil + }), + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "share id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "share id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing required resourcePoolId", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiUpdateShareRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}, sfs.NullableString{}, sfs.NullableInt32{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + resourcePoolLabel string + item *sfs.UpdateShareResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + item: &sfs.UpdateShareResponse{}, + }, + wantErr: false, + }, + { + name: "set empty share", + args: args{ + item: &sfs.UpdateShareResponse{ + Share: &sfs.Share{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.resourcePoolLabel, tt.args.item); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/describe/describe.go b/internal/cmd/beta/sfs/snapshot-policy/describe/describe.go new file mode 100644 index 000000000..2123ac0c6 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/describe/describe.go @@ -0,0 +1,149 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const snapshotPolicyIdArg = "SNAPSHOT_POLICY_ID" + +type inputModel struct { + *globalflags.GlobalFlagModel + SnapshotPolicyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", snapshotPolicyIdArg), + Short: "Shows details of a snapshot policy", + Long: "Shows details of a snapshot policy.", + Args: args.SingleArg(snapshotPolicyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a snapshot policy with ID "xxx"`, + "$ stackit beta sfs snapshot-policy describe xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read snapshot policy: %w", err) + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + return outputResult(params.Printer, model.OutputFormat, model.SnapshotPolicyId, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + snapshotPolicyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SnapshotPolicyId: snapshotPolicyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetSnapshotPolicyRequest { + return apiClient.DefaultAPI.GetSnapshotPolicy(ctx, model.ProjectId, model.SnapshotPolicyId) +} + +func outputResult(p *print.Printer, outputFormat, snapshotPolicyId, projectLabel string, snapshotPolicy *sfs.GetSnapshotPolicyResponse) error { + return p.OutputResult(outputFormat, snapshotPolicy, func() error { + if snapshotPolicy == nil || snapshotPolicy.SnapshotPolicy == nil { + p.Outputf("Snapshot policy %q not found in project %q", snapshotPolicyId, projectLabel) + return nil + } + + var content []tables.Table + + table := tables.NewTable() + table.SetTitle("Snapshot Policy") + policy := snapshotPolicy.SnapshotPolicy + + table.AddRow("ID", utils.PtrString(policy.Id)) + table.AddSeparator() + table.AddRow("NAME", utils.PtrString(policy.Name)) + table.AddSeparator() + table.AddRow("ENABLED", utils.PtrString(policy.Enabled)) + table.AddSeparator() + table.AddRow("COMMENT", utils.PtrString(policy.Comment)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(policy.CreatedAt)) + + content = append(content, table) + + if len(policy.SnapshotSchedules) > 0 { + snapshotSchedulesTable := tables.NewTable() + snapshotSchedulesTable.SetTitle("Snapshot Schedules") + + snapshotSchedulesTable.SetHeader("ID", "NAME", "INTERVAL", "PREFIX", "RETENTION COUNT", "RETENTION PERIOD", "CREATED AT") + + for _, snapshotSchedule := range policy.SnapshotSchedules { + snapshotSchedulesTable.AddRow( + utils.PtrString(snapshotSchedule.Id), + utils.PtrString(snapshotSchedule.Name), + utils.PtrString(snapshotSchedule.Interval), + utils.PtrString(snapshotSchedule.Prefix), + utils.PtrString(snapshotSchedule.RetentionCount), + utils.PtrString(snapshotSchedule.RetentionPeriod), + utils.ConvertTimePToDateTimeString(snapshotSchedule.CreatedAt), + ) + snapshotSchedulesTable.AddSeparator() + } + + content = append(content, snapshotSchedulesTable) + } + + if err := tables.DisplayTables(p, content); err != nil { + return fmt.Errorf("render tables: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go b/internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go new file mode 100644 index 000000000..10e69067a --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/describe/describe_test.go @@ -0,0 +1,221 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" +var testSnapshotPolicyId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testSnapshotPolicyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + SnapshotPolicyId: testSnapshotPolicyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetSnapshotPolicyRequest)) sfs.ApiGetSnapshotPolicyRequest { + request := testClient.DefaultAPI.GetSnapshotPolicy(testCtx, testProjectId, testSnapshotPolicyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "snapshot policy id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "snapshot policy id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetSnapshotPolicyRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + snapshotPolicyId string + projectLabel string + snapshotPolicy *sfs.GetSnapshotPolicyResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty snapshot policy", + args: args{ + snapshotPolicy: &sfs.GetSnapshotPolicyResponse{}, + }, + wantErr: false, + }, + { + name: "set empty snapshot policy", + args: args{ + snapshotPolicy: &sfs.GetSnapshotPolicyResponse{ + SnapshotPolicy: &sfs.SnapshotPolicy{}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.snapshotPolicyId, tt.args.projectLabel, tt.args.snapshotPolicy); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/list/list.go b/internal/cmd/beta/sfs/snapshot-policy/list/list.go new file mode 100644 index 000000000..d343216f2 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/list/list.go @@ -0,0 +1,180 @@ +package list + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + limitFlag = "limit" +) + +var immutableFlag = flags.StringEnumFlag( + "immutable", + []string{"all", "immutable-only", "mutable-only"}, + "Immutable snapshot policy,", + flags.StringEnumDefaultValue("all"), + flags.StringEnumIgnoreCase[string](), +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 + Immutable *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all snapshot policies of a project", + Long: "Lists all snapshot policies of a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all snapshot policies`, + "$ stackit beta sfs snapshot-policy list", + ), + examples.NewExample( + `List only mutable snapshot policies`, + "$ stackit beta sfs snapshot-policy list --immutable mutable-only", + ), + examples.NewExample( + `List only immutable snapshot policies`, + "$ stackit beta sfs snapshot-policy list --immutable immutable-only", + ), + examples.NewExample( + `List up to 10 snapshot policies`, + "$ stackit beta sfs snapshot-policy list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return fmt.Errorf("unable to parse input: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list snapshot policies: %w", err) + } + + // Get projectLabel + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.SnapshotPolicies) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + immutableFlag.Register(cmd.Flags()) + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + Immutable: immutableFlag.Ptr(), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiListSnapshotPoliciesRequest { + req := apiClient.DefaultAPI.ListSnapshotPolicies(ctx, model.ProjectId) + + if model.Immutable != nil { + switch *model.Immutable { + case "all": + return req + case "mutable-only": + req = req.Immutable(false) + case "immutable-only": + req = req.Immutable(true) + } + } + return req +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, snapshotPolicies []sfs.SnapshotPolicy) error { + return p.OutputResult(outputFormat, snapshotPolicies, func() error { + if len(snapshotPolicies) == 0 { + p.Outputf("No snapshot policies found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "COMMENT", "ENABLED", "AMOUNT OF SNAPSHOT SCHEDULES", "CREATED AT") + + for _, snapshotPolicy := range snapshotPolicies { + amountSnapshotSchedules := "-" + if snapshotPolicy.SnapshotSchedules != nil { + amountSnapshotSchedules = strconv.Itoa(len(snapshotPolicy.SnapshotSchedules)) + } + table.AddRow( + utils.PtrString(snapshotPolicy.Id), + utils.PtrString(snapshotPolicy.Name), + utils.PtrString(snapshotPolicy.Comment), + utils.PtrString(snapshotPolicy.Enabled), + amountSnapshotSchedules, + utils.ConvertTimePToDateTimeString(snapshotPolicy.CreatedAt), + ) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/list/list_test.go b/internal/cmd/beta/sfs/snapshot-policy/list/list_test.go new file mode 100644 index 000000000..6106bc1c6 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/list/list_test.go @@ -0,0 +1,235 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + limitFlag: strconv.Itoa(10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Immutable: utils.Ptr("all"), + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListSnapshotPoliciesRequest)) sfs.ApiListSnapshotPoliciesRequest { + request := testClient.DefaultAPI.ListSnapshotPolicies(testCtx, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "immutable snapshot policies", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[immutableFlag.Name()] = "immutable-only" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Immutable = utils.Ptr("immutable-only") + }), + }, + { + description: "mutable snapshot policies", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[immutableFlag.Name()] = "mutable-only" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Immutable = utils.Ptr("mutable-only") + }), + }, + { + description: "all snapshot policies", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[immutableFlag.Name()] = "all" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Immutable = utils.Ptr("all") + }), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiListSnapshotPoliciesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "only immutable snapshot policies", + model: fixtureInputModel(func(model *inputModel) { + model.Immutable = utils.Ptr("immutable-only") + }), + expectedRequest: fixtureRequest().Immutable(true), + }, + { + description: "only mutable snapshot policies", + model: fixtureInputModel(func(model *inputModel) { + model.Immutable = utils.Ptr("mutable-only") + }), + expectedRequest: fixtureRequest().Immutable(false), + }, + { + description: "all snapshot policies", + model: fixtureInputModel(func(model *inputModel) { + model.Immutable = utils.Ptr("all") + }), + expectedRequest: fixtureRequest(), + }, + { + description: "all snapshot policies - without immutable flag", + model: fixtureInputModel(func(model *inputModel) { + model.Immutable = nil + }), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + snapshotPolicies []sfs.SnapshotPolicy + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty snapshot policy", + args: args{ + snapshotPolicies: []sfs.SnapshotPolicy{ + {}, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.snapshotPolicies); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go b/internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go new file mode 100644 index 000000000..f45eaa1a9 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot-policy/snapshot-policy.go @@ -0,0 +1,27 @@ +package snapshotpolicy + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot-policy/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot-policy/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshot-policy", + Short: "Provides functionality for SFS snapshot policies", + Long: "Provides functionality for SFS snapshot policies.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/snapshot/create/create.go b/internal/cmd/beta/sfs/snapshot/create/create.go new file mode 100644 index 000000000..b630a87d7 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/create/create.go @@ -0,0 +1,155 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + resourcePoolIdFlag = "resource-pool-id" + nameFlag = "name" + commentFlag = "comment" + snaplockRetentionHoursFlag = "snaplock-retention-hours" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + Name string + Comment *string + SnaplockRetentionHours *int32 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new snapshot of a resource pool", + Long: "Creates a new snapshot of a resource pool.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new snapshot with name "snapshot-name" of a resource pool with ID "xxx"`, + "$ stackit beta sfs snapshot create --name snapshot-name --resource-pool-id xxx", + ), + examples.NewExample( + `Create a new snapshot with name "snapshot-name" and comment "snapshot-comment" of a resource pool with ID "xxx"`, + `$ stackit beta sfs snapshot create --name snapshot-name --resource-pool-id xxx --comment "snapshot-comment"`, + ), + examples.NewExample( + `Create a new snapshot with name "snapshot-name" and snaplock retention hours "24" of a resource pool with ID "xxx"`, + `$ stackit beta sfs snapshot create --name snapshot-name --resource-pool-id xxx --snaplock-retention-hours 24`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to create a snapshot for resource pool %q?", resourcePoolLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create snapshot: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.Name, resourcePoolLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(nameFlag, "", "Snapshot name") + cmd.Flags().String(commentFlag, "", "A comment to add more information to the snapshot") + cmd.Flags().Int32(snaplockRetentionHoursFlag, 0, "Retention hours for the snaplock") + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool from which the snapshot should be created") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag, nameFlag) + cobra.CheckErr(err) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiCreateResourcePoolSnapshotRequest { + req := apiClient.DefaultAPI.CreateResourcePoolSnapshot(ctx, model.ProjectId, model.Region, model.ResourcePoolId) + req = req.CreateResourcePoolSnapshotPayload(sfs.CreateResourcePoolSnapshotPayload{ + Name: utils.Ptr(model.Name), + Comment: *sfs.NewNullableString(model.Comment), + SnaplockRetentionHours: *sfs.NewNullableInt32(model.SnaplockRetentionHours), + }) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + Comment: flags.FlagToStringPointer(p, cmd, commentFlag), + SnaplockRetentionHours: flags.FlagToInt32Pointer(p, cmd, snaplockRetentionHoursFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, snapshotLabel, resourcePoolLabel string, resp *sfs.CreateResourcePoolSnapshotResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil || resp.ResourcePoolSnapshot == nil { + p.Outputln("SFS snapshot response is empty") + return nil + } + + p.Outputf( + "Created snapshot %q for resource pool %q.\n", + snapshotLabel, + resourcePoolLabel, + ) + + if resp.ResourcePoolSnapshot.SnaplockExpiryTime.IsSet() && resp.ResourcePoolSnapshot.SnaplockExpiryTime.Get() != nil { + p.Outputf("Snaplock expiry time: %s\n", utils.ConvertTimePToDateTimeString(resp.ResourcePoolSnapshot.SnaplockExpiryTime.Get())) + } + + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot/create/create_test.go b/internal/cmd/beta/sfs/snapshot/create/create_test.go new file mode 100644 index 000000000..63a36caa6 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/create/create_test.go @@ -0,0 +1,239 @@ +package create + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testName = "test-name" +var testComment = "test-comment" +var testSnaplockRetentionHours int32 = 24 +var testResourcePoolId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + nameFlag: testName, + resourcePoolIdFlag: testResourcePoolId, + commentFlag: testComment, + snaplockRetentionHoursFlag: strconv.Itoa(int(testSnaplockRetentionHours)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: testName, + ResourcePoolId: testResourcePoolId, + Comment: utils.Ptr(testComment), + SnaplockRetentionHours: utils.Ptr(testSnaplockRetentionHours), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiCreateResourcePoolSnapshotRequest)) sfs.ApiCreateResourcePoolSnapshotRequest { + request := testClient.DefaultAPI.CreateResourcePoolSnapshot(testCtx, testProjectId, testRegion, testResourcePoolId) + request = request.CreateResourcePoolSnapshotPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(request *sfs.CreateResourcePoolSnapshotPayload)) sfs.CreateResourcePoolSnapshotPayload { + payload := sfs.CreateResourcePoolSnapshotPayload{ + Name: utils.Ptr(testName), + Comment: *sfs.NewNullableString( + utils.Ptr(testComment), + ), + SnaplockRetentionHours: *sfs.NewNullableInt32( + utils.Ptr(testSnaplockRetentionHours), + ), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "required only", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commentFlag) + delete(flagValues, snaplockRetentionHoursFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Comment = nil + model.SnaplockRetentionHours = nil + }), + }, + { + description: "missing required name", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "missing required resourcePoolId", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + { + description: "invalid resource pool id 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid resource pool id 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "invalid-resource-pool-id" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiCreateResourcePoolSnapshotRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(sfs.NullableString{}, sfs.NullableInt32{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + snapshotLabel string + resourcePoolLabel string + resp *sfs.CreateResourcePoolSnapshotResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + resp: &sfs.CreateResourcePoolSnapshotResponse{}, + }, + wantErr: false, + }, + { + name: "set empty snapshot", + args: args{ + resp: &sfs.CreateResourcePoolSnapshotResponse{ + ResourcePoolSnapshot: &sfs.ResourcePoolSnapshot{}, + }, + }, + wantErr: false, + }, + { + name: "set full snapshot", + args: args{ + resp: &sfs.CreateResourcePoolSnapshotResponse{ + ResourcePoolSnapshot: &sfs.ResourcePoolSnapshot{ + SnaplockExpiryTime: *sfs.NewNullableTime(utils.Ptr(time.Now().Add(time.Hour))), + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.snapshotLabel, tt.args.resourcePoolLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot/delete/delete.go b/internal/cmd/beta/sfs/snapshot/delete/delete.go new file mode 100644 index 000000000..7fd570bd6 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/delete/delete.go @@ -0,0 +1,113 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const ( + snapshotNameArg = "SNAPSHOT_NAME" + + resourcePoolIdFlag = "resource-pool-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + SnapshotName string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", snapshotNameArg), + Short: "Deletes a snapshot", + Long: "Deletes a snapshot.", + Args: args.SingleArg(snapshotNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Delete a snapshot with "SNAPSHOT_NAME" from resource pool with ID "yyy"`, + "$ stackit beta sfs snapshot delete SNAPSHOT_NAME --resource-pool-id yyy"), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q for resource pool %q?", model.SnapshotName, resourcePoolLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete snapshot: %w", err) + } + + params.Printer.Outputf("Deleted snapshot %q from resource pool %q.\n", model.SnapshotName, resourcePoolLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool from which the snapshot should be created") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiDeleteResourcePoolSnapshotRequest { + return apiClient.DefaultAPI.DeleteResourcePoolSnapshot(ctx, model.ProjectId, model.Region, model.ResourcePoolId, model.SnapshotName) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + snapshotName := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SnapshotName: snapshotName, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/beta/sfs/snapshot/delete/delete_test.go b/internal/cmd/beta/sfs/snapshot/delete/delete_test.go new file mode 100644 index 000000000..ebc37fc03 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/delete/delete_test.go @@ -0,0 +1,189 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() +var testSnapshotName = "testSnapshot" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testSnapshotName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + SnapshotName: testSnapshotName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiDeleteResourcePoolSnapshotRequest)) sfs.ApiDeleteResourcePoolSnapshotRequest { + request := testClient.DefaultAPI.DeleteResourcePoolSnapshot(testCtx, testProjectId, testRegion, testResourcePoolId, testSnapshotName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "share id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "resource pool invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "" + }), + isValid: false, + }, + { + description: "resource pool invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiDeleteResourcePoolSnapshotRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot/describe/describe.go b/internal/cmd/beta/sfs/snapshot/describe/describe.go new file mode 100644 index 000000000..359701838 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/describe/describe.go @@ -0,0 +1,136 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + snapshotNameArg = "SNAPSHOT_NAME" + + resourcePoolIdFlag = "resource-pool-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + SnapshotName string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", snapshotNameArg), + Short: "Shows details of a snapshot", + Long: "Shows details of a snapshot.", + Args: args.SingleArg(snapshotNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Describe a snapshot with "SNAPSHOT_NAME" from resource pool with ID "yyy"`, + "stackit beta sfs snapshot describe SNAPSHOT_NAME --resource-pool-id yyy", + ), + ), + RunE: func(cmd *cobra.Command, inputArgs []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, inputArgs) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create snapshot: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool from which the snapshot should be created") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiGetResourcePoolSnapshotRequest { + return apiClient.DefaultAPI.GetResourcePoolSnapshot(ctx, model.ProjectId, model.Region, model.ResourcePoolId, model.SnapshotName) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + snapshotName := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SnapshotName: snapshotName, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, resp *sfs.GetResourcePoolSnapshotResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil || resp.ResourcePoolSnapshot == nil { + p.Outputln("Resource pool snapshot response is empty") + return nil + } + + table := tables.NewTable() + + snap := *resp.ResourcePoolSnapshot + var snaplockExpiryTime string + if snap.SnaplockExpiryTime.IsSet() && snap.SnaplockExpiryTime.Get() != nil { + snaplockExpiryTime = utils.ConvertTimePToDateTimeString(snap.SnaplockExpiryTime.Get()) + } + table.AddRow("NAME", utils.PtrString(snap.SnapshotName)) + table.AddSeparator() + if snap.Comment.IsSet() && snap.Comment.Get() != nil { + table.AddRow("COMMENT", utils.PtrString(snap.Comment.Get())) + table.AddSeparator() + } + table.AddRow("RESOURCE POOL ID", utils.PtrString(snap.ResourcePoolId)) + table.AddSeparator() + table.AddRow("SIZE (GB)", utils.PtrString(snap.SizeGigabytes)) + table.AddSeparator() + table.AddRow("LOGICAL SIZE (GB)", utils.PtrString(snap.LogicalSizeGigabytes)) + table.AddSeparator() + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(snap.CreatedAt)) + table.AddSeparator() + table.AddRow("SNAPLOCK EXPIRY TIME", snaplockExpiryTime) + table.AddSeparator() + + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot/describe/describe_test.go b/internal/cmd/beta/sfs/snapshot/describe/describe_test.go new file mode 100644 index 000000000..4c086c056 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/describe/describe_test.go @@ -0,0 +1,251 @@ +package describe + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() +var testSnapshotName = "testSnapshotName" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testSnapshotName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + SnapshotName: testSnapshotName, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiGetResourcePoolSnapshotRequest)) sfs.ApiGetResourcePoolSnapshotRequest { + request := testClient.DefaultAPI.GetResourcePoolSnapshot(testCtx, testProjectId, testRegion, testResourcePoolId, testSnapshotName) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "share id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "resource pool invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "" + }), + isValid: false, + }, + { + description: "resource pool invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiGetResourcePoolSnapshotRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resp *sfs.GetResourcePoolSnapshotResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + resp: &sfs.GetResourcePoolSnapshotResponse{}, + }, + wantErr: false, + }, + { + name: "set empty snapshot", + args: args{ + resp: &sfs.GetResourcePoolSnapshotResponse{ + ResourcePoolSnapshot: &sfs.ResourcePoolSnapshot{}, + }, + }, + wantErr: false, + }, + { + name: "set full snapshot", + args: args{ + resp: &sfs.GetResourcePoolSnapshotResponse{ + ResourcePoolSnapshot: &sfs.ResourcePoolSnapshot{ + SnapshotName: utils.Ptr("name"), + ResourcePoolId: utils.Ptr("rp-id"), + SizeGigabytes: utils.Ptr(int32(10)), + LogicalSizeGigabytes: utils.Ptr(int32(8)), + CreatedAt: utils.Ptr(time.Now()), + SnaplockExpiryTime: *sfs.NewNullableTime(utils.Ptr(time.Now().Add(time.Hour))), + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot/list/list.go b/internal/cmd/beta/sfs/snapshot/list/list.go new file mode 100644 index 000000000..418434846 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/list/list.go @@ -0,0 +1,155 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + resourcePoolIdFlag = "resource-pool-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all snapshots of a resource pool", + Long: "Lists all snapshots of a resource pool.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all snapshots of a resource pool with ID "xxx"`, + "$ stackit beta sfs snapshot list --resource-pool-id xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list snapshot: %w", err) + } + + // Truncate output + items := utils.GetSliceFromPointer(&resp.ResourcePoolSnapshots) + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool from which the snapshot should be created") + cmd.Flags().Int64(limitFlag, 0, "Number of snapshots to list") + + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiListResourcePoolSnapshotsRequest { + req := apiClient.DefaultAPI.ListResourcePoolSnapshots(ctx, model.ProjectId, model.Region, model.ResourcePoolId) + return req +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, resp []sfs.ResourcePoolSnapshot) error { + return p.OutputResult(outputFormat, resp, func() error { + if len(resp) == 0 { + p.Outputln("No snapshots found") + return nil + } + table := tables.NewTable() + table.SetHeader( + "NAME", + "COMMENT", + "RESOURCE POOL ID", + "SIZE (GB)", + "LOGICAL SIZE (GB)", + "CREATED AT", + "SNAPLOCK EXPIRY TIME", + ) + + for _, snap := range resp { + var comment string + if snap.Comment.IsSet() && snap.Comment.Get() != nil { + comment = utils.PtrString(snap.Comment.Get()) + } + var snaplockExpiryTime string + if snap.SnaplockExpiryTime.IsSet() && snap.SnaplockExpiryTime.Get() != nil { + snaplockExpiryTime = utils.ConvertTimePToDateTimeString(snap.SnaplockExpiryTime.Get()) + } + table.AddRow( + utils.PtrString(snap.SnapshotName), + comment, + utils.PtrString(snap.ResourcePoolId), + utils.PtrString(snap.SizeGigabytes), + utils.PtrString(snap.LogicalSizeGigabytes), + utils.ConvertTimePToDateTimeString(snap.CreatedAt), + snaplockExpiryTime, + ) + } + + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot/list/list_test.go b/internal/cmd/beta/sfs/snapshot/list/list_test.go new file mode 100644 index 000000000..ede7e22d1 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/list/list_test.go @@ -0,0 +1,191 @@ +package list + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testResourcePoolId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + resourcePoolIdFlag: testResourcePoolId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ResourcePoolId: testResourcePoolId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiListResourcePoolSnapshotsRequest)) sfs.ApiListResourcePoolSnapshotsRequest { + request := testClient.DefaultAPI.ListResourcePoolSnapshots(testCtx, testProjectId, testRegion, testResourcePoolId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flags", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid resource pool id 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid resource pool id 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "invalid-resource-pool-id" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiListResourcePoolSnapshotsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + resp []sfs.ResourcePoolSnapshot + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + resp: []sfs.ResourcePoolSnapshot{}, + }, + wantErr: false, + }, + { + name: "set empty snapshot", + args: args{ + resp: []sfs.ResourcePoolSnapshot{{}}, + }, + wantErr: false, + }, + { + name: "set full snapshot", + args: args{ + resp: []sfs.ResourcePoolSnapshot{ + { + SnapshotName: utils.Ptr("name"), + ResourcePoolId: utils.Ptr("rp-id"), + SizeGigabytes: utils.Ptr(int32(10)), + LogicalSizeGigabytes: utils.Ptr(int32(8)), + CreatedAt: utils.Ptr(time.Now()), + SnaplockExpiryTime: *sfs.NewNullableTime(utils.Ptr(time.Now().Add(time.Hour))), + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sfs/snapshot/snapshot.go b/internal/cmd/beta/sfs/snapshot/snapshot.go new file mode 100644 index 000000000..443d96df6 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/snapshot.go @@ -0,0 +1,34 @@ +package snapshot + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sfs/snapshot/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshot", + Short: "Provides functionality for SFS snapshots", + Long: "Provides functionality for SFS snapshots.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/beta/sfs/snapshot/update/update.go b/internal/cmd/beta/sfs/snapshot/update/update.go new file mode 100644 index 000000000..d39747431 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/update/update.go @@ -0,0 +1,156 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/client" + sfsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sfs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + snapshotNameArg = "SNAPSHOT_NAME" + + resourcePoolIdFlag = "resource-pool-id" + newSnapshotNameFlag = "name" + commentFlag = "comment" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ResourcePoolId string + SnapshotName string + NewSnapshotName string + Comment *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", snapshotNameArg), + Short: "Updates a new snapshot of a resource pool", + Long: "Updates a new snapshot of a resource pool.", + Args: args.SingleArg(snapshotNameArg, nil), + Example: examples.Build( + examples.NewExample( + `Updates the name of a snapshot with name "snapshot-name" of a resource pool with ID "xxx"`, + "$ stackit beta sfs snapshot update snapshot-name --resource-pool-id xxx --name new-snapshot-name", + ), + examples.NewExample( + `Updates the comment of a snapshot with name "snapshot-name" of a resource pool with ID "xxx"`, + `$ stackit beta sfs snapshot update snapshot-name --resource-pool-id xxx --comment "snapshot-comment"`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + resourcePoolLabel, err := sfsUtils.GetResourcePoolName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ResourcePoolId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get resource pool name: %v", err) + resourcePoolLabel = model.ResourcePoolId + } else if resourcePoolLabel == "" { + resourcePoolLabel = model.ResourcePoolId + } + + prompt := fmt.Sprintf("Are you sure you want to update the snapshot %q for resource pool %q?", model.SnapshotName, resourcePoolLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update snapshot: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.SnapshotName, resourcePoolLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(newSnapshotNameFlag, "", "Snapshot name") + cmd.Flags().String(commentFlag, "", "A comment to add more information to the snapshot") + cmd.Flags().Var(flags.UUIDFlag(), resourcePoolIdFlag, "The resource pool from which the snapshot should be updated") + + cmd.MarkFlagsOneRequired(newSnapshotNameFlag, commentFlag) + err := flags.MarkFlagsRequired(cmd, resourcePoolIdFlag) + cobra.CheckErr(err) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *sfs.APIClient) sfs.ApiUpdateResourcePoolSnapshotRequest { + req := apiClient.DefaultAPI.UpdateResourcePoolSnapshot(ctx, model.ProjectId, model.Region, model.ResourcePoolId, model.SnapshotName) + + payload := sfs.UpdateResourcePoolSnapshotPayload{ + Comment: *sfs.NewNullableString(model.Comment), + } + + if model.NewSnapshotName != "" { + payload.Name = *sfs.NewNullableString(utils.Ptr(model.NewSnapshotName)) + } + return req.UpdateResourcePoolSnapshotPayload(payload) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + snapshotName := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + SnapshotName: snapshotName, + NewSnapshotName: flags.FlagToStringValue(p, cmd, newSnapshotNameFlag), + ResourcePoolId: flags.FlagToStringValue(p, cmd, resourcePoolIdFlag), + Comment: flags.FlagToStringPointer(p, cmd, commentFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, snapshotLabel, resourcePoolLabel string, resp *sfs.UpdateResourcePoolSnapshotResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil || resp.ResourcePoolSnapshot == nil { + p.Outputln("SFS snapshot response is empty") + return nil + } + + p.Outputf( + "Updated snapshot %q for resource pool %q.\n", + snapshotLabel, + resourcePoolLabel, + ) + + if resp.ResourcePoolSnapshot.SnaplockExpiryTime.IsSet() && resp.ResourcePoolSnapshot.SnaplockExpiryTime.Get() != nil { + p.Outputf("Snaplock expiry time: %s\n", utils.ConvertTimePToDateTimeString(resp.ResourcePoolSnapshot.SnaplockExpiryTime.Get())) + } + + return nil + }) +} diff --git a/internal/cmd/beta/sfs/snapshot/update/update_test.go b/internal/cmd/beta/sfs/snapshot/update/update_test.go new file mode 100644 index 000000000..7de6d5c29 --- /dev/null +++ b/internal/cmd/beta/sfs/snapshot/update/update_test.go @@ -0,0 +1,260 @@ +package update + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var projectIdFlag = globalflags.ProjectIdFlag +var regionFlag = globalflags.RegionFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &sfs.APIClient{DefaultAPI: &sfs.DefaultAPIService{}} + +var testProjectId = uuid.NewString() +var testRegion = "eu01" + +var testSnapshotName = "test-snapshot-name" +var testNewName = "test-new-name" +var testComment = "test-comment" +var testResourcePoolId = uuid.NewString() + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testSnapshotName, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + regionFlag: testRegion, + + newSnapshotNameFlag: testNewName, + resourcePoolIdFlag: testResourcePoolId, + commentFlag: testComment, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + SnapshotName: testSnapshotName, + NewSnapshotName: testNewName, + ResourcePoolId: testResourcePoolId, + Comment: utils.Ptr(testComment), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *sfs.ApiUpdateResourcePoolSnapshotRequest)) sfs.ApiUpdateResourcePoolSnapshotRequest { + request := testClient.DefaultAPI.UpdateResourcePoolSnapshot(testCtx, testProjectId, testRegion, testResourcePoolId, testSnapshotName) + request = request.UpdateResourcePoolSnapshotPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(request *sfs.UpdateResourcePoolSnapshotPayload)) sfs.UpdateResourcePoolSnapshotPayload { + payload := sfs.UpdateResourcePoolSnapshotPayload{ + Name: *sfs.NewNullableString(utils.Ptr(testNewName)), + Comment: *sfs.NewNullableString( + utils.Ptr(testComment), + ), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "either name or comment (only name set)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, commentFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Comment = nil + }), + }, + { + description: "either name or comment (only comment set)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, newSnapshotNameFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NewSnapshotName = "" + }), + }, + { + description: "missing both name and comment (at least one required)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, newSnapshotNameFlag) + delete(flagValues, commentFlag) + }), + isValid: false, + }, + { + description: "missing required resourcePoolId", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, resourcePoolIdFlag) + }), + isValid: false, + }, + { + description: "invalid resource pool id 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid resource pool id 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[resourcePoolIdFlag] = "invalid-resource-pool-id" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest sfs.ApiUpdateResourcePoolSnapshotRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, sfs.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + cmp.AllowUnexported(sfs.NullableString{}, sfs.NullableInt32{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + snapshotLabel string + resourcePoolLabel string + resp *sfs.UpdateResourcePoolSnapshotResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + resp: &sfs.UpdateResourcePoolSnapshotResponse{}, + }, + wantErr: false, + }, + { + name: "set empty snapshot", + args: args{ + resp: &sfs.UpdateResourcePoolSnapshotResponse{ + ResourcePoolSnapshot: &sfs.ResourcePoolSnapshot{}, + }, + }, + wantErr: false, + }, + { + name: "set snaplock expiry time", + args: args{ + resp: &sfs.UpdateResourcePoolSnapshotResponse{ + ResourcePoolSnapshot: &sfs.ResourcePoolSnapshot{ + SnaplockExpiryTime: *sfs.NewNullableTime(utils.Ptr(time.Now())), + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.snapshotLabel, tt.args.resourcePoolLabel, tt.args.resp); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/sqlserverflex/database/create/create.go b/internal/cmd/beta/sqlserverflex/database/create/create.go index f8ae03ccd..d15e4f06c 100644 --- a/internal/cmd/beta/sqlserverflex/database/create/create.go +++ b/internal/cmd/beta/sqlserverflex/database/create/create.go @@ -2,11 +2,12 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" "github.com/spf13/cobra" ) @@ -34,7 +34,7 @@ type inputModel struct { Owner string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("create %s", databaseNameArg), Short: "Creates a SQLServer Flex database", @@ -61,24 +61,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create database %q? (This cannot be undone)", model.DatabaseName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create database %q? (This cannot be undone)", model.DatabaseName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API req := buildRequest(ctx, model, apiClient) - s := spinner.New(params.Printer) - s.Start("Creating database") - resp, err := req.Execute() + resp, err := spinner.Run2(params.Printer, "Creating database", req.Execute) if err != nil { - s.StopWithError() return fmt.Errorf("create SQLServer Flex database: %w", err) } - s.Stop() return outputResult(params.Printer, model.OutputFormat, model.DatabaseName, resp) }, @@ -109,24 +103,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Owner: flags.FlagToStringValue(p, cmd, ownerFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiCreateDatabaseRequest { - req := apiClient.CreateDatabase(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.CreateDatabase(ctx, model.ProjectId, model.InstanceId, model.Region) payload := sqlserverflex.CreateDatabasePayload{ - Name: &model.DatabaseName, - Options: &sqlserverflex.DatabaseDocumentationCreateDatabaseRequestOptions{ - Owner: &model.Owner, + Name: model.DatabaseName, + Options: sqlserverflex.DatabaseDocumentationCreateDatabaseRequestOptions{ + Owner: model.Owner, }, } req = req.CreateDatabasePayload(payload) @@ -137,25 +123,9 @@ func outputResult(p *print.Printer, outputFormat, databaseName string, resp *sql if resp == nil { return fmt.Errorf("sqlserverflex response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex database: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex database: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { p.Outputf("Created database %q\n", databaseName) return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/database/create/create_test.go b/internal/cmd/beta/sqlserverflex/database/create/create_test.go index c6359313e..6d43c2eac 100644 --- a/internal/cmd/beta/sqlserverflex/database/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/database/create/create_test.go @@ -7,16 +7,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testDatabaseName = "my-database" @@ -64,11 +67,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateDatabaseRequest)) sqlserverflex.ApiCreateDatabaseRequest { - request := testClient.CreateDatabase(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.CreateDatabase(testCtx, testProjectId, testInstanceId, testRegion) payload := sqlserverflex.CreateDatabasePayload{ - Name: &testDatabaseName, - Options: &sqlserverflex.DatabaseDocumentationCreateDatabaseRequestOptions{ - Owner: &testOwner, + Name: testDatabaseName, + Options: sqlserverflex.DatabaseDocumentationCreateDatabaseRequestOptions{ + Owner: testOwner, }, } request = request.CreateDatabasePayload(payload) @@ -177,54 +180,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -248,7 +204,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -281,11 +237,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.databaseName, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.databaseName, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/database/database.go b/internal/cmd/beta/sqlserverflex/database/database.go index f11cf2bd0..75113d255 100644 --- a/internal/cmd/beta/sqlserverflex/database/database.go +++ b/internal/cmd/beta/sqlserverflex/database/database.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/database/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "database", Short: "Provides functionality for SQLServer Flex databases", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/beta/sqlserverflex/database/delete/delete.go b/internal/cmd/beta/sqlserverflex/database/delete/delete.go index 0f2b221f7..289d6ef47 100644 --- a/internal/cmd/beta/sqlserverflex/database/delete/delete.go +++ b/internal/cmd/beta/sqlserverflex/database/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) const ( @@ -30,7 +31,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", databaseNameArg), Short: "Deletes a SQLServer Flex database", @@ -57,24 +58,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete database %q? (This cannot be undone)", model.DatabaseName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete database %q? (This cannot be undone)", model.DatabaseName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API req := buildRequest(ctx, model, apiClient) - s := spinner.New(params.Printer) - s.Start("Deleting database") - err = req.Execute() + err = spinner.Run(params.Printer, "Deleting database", func() error { + err := req.Execute() + return err + }) if err != nil { - s.StopWithError() return fmt.Errorf("delete SQLServer Flex database: %w", err) } - s.Stop() params.Printer.Info("Deleted database %q\n", model.DatabaseName) return nil @@ -104,19 +102,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiDeleteDatabaseRequest { - req := apiClient.DeleteDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName, model.Region) + req := apiClient.DefaultAPI.DeleteDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName, model.Region) return req } diff --git a/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go b/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go index 53a099f48..aea7de825 100644 --- a/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go +++ b/internal/cmd/beta/sqlserverflex/database/delete/delete_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -17,7 +17,8 @@ import ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testDatabaseName = "my-database" @@ -62,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteDatabaseRequest)) sqlserverflex.ApiDeleteDatabaseRequest { - request := testClient.DeleteDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName, testRegion) + request := testClient.DefaultAPI.DeleteDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName, testRegion) for _, mod := range mods { mod(&request) } @@ -160,54 +161,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -231,7 +185,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/beta/sqlserverflex/database/describe/describe.go b/internal/cmd/beta/sqlserverflex/database/describe/describe.go index 377d90476..80c224173 100644 --- a/internal/cmd/beta/sqlserverflex/database/describe/describe.go +++ b/internal/cmd/beta/sqlserverflex/database/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) const ( @@ -32,7 +32,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", databaseNameArg), Short: "Shows details of an SQLServer Flex database", @@ -93,20 +93,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiGetDatabaseRequest { - req := apiClient.GetDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName, model.Region) + req := apiClient.DefaultAPI.GetDatabase(ctx, model.ProjectId, model.InstanceId, model.DatabaseName, model.Region) return req } @@ -114,24 +106,8 @@ func outputResult(p *print.Printer, outputFormat string, resp *sqlserverflex.Get if resp == nil || resp.Database == nil { return fmt.Errorf("database response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex database: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex database: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { database := resp.Database table := tables.NewTable() table.AddRow("ID", utils.PtrString(database.Id)) @@ -157,5 +133,5 @@ func outputResult(p *print.Printer, outputFormat string, resp *sqlserverflex.Get } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go index 919b1e3c5..585d42d90 100644 --- a/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go +++ b/internal/cmd/beta/sqlserverflex/database/describe/describe_test.go @@ -7,16 +7,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testDatabaseName = "my-database" @@ -61,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiGetDatabaseRequest)) sqlserverflex.ApiGetDatabaseRequest { - request := testClient.GetDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName, testRegion) + request := testClient.DefaultAPI.GetDatabase(testCtx, testProjectId, testInstanceId, testDatabaseName, testRegion) for _, mod := range mods { mod(&request) } @@ -159,54 +162,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -230,7 +186,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -269,11 +225,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/database/list/list.go b/internal/cmd/beta/sqlserverflex/database/list/list.go index 707922ef9..a9ad35d87 100644 --- a/internal/cmd/beta/sqlserverflex/database/list/list.go +++ b/internal/cmd/beta/sqlserverflex/database/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) const ( @@ -32,7 +32,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all SQLServer Flex databases", @@ -49,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 SQLServer Flex databases of instance with ID "xxx"`, "$ stackit beta sqlserverflex database list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,23 +68,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get SQLServer Flex databases: %w", err) } - if resp.Databases == nil || len(*resp.Databases) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No databases found for instance %s on project %s\n", model.InstanceId, projectLabel) - return nil + databases := resp.GetDatabases() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - databases := *resp.Databases // Truncate output if model.Limit != nil && len(databases) > int(*model.Limit) { databases = databases[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, databases) + return outputResult(params.Printer, model.OutputFormat, model.InstanceId, projectLabel, databases) }, } @@ -100,7 +97,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -120,42 +117,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiListDatabasesRequest { - req := apiClient.ListDatabases(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.ListDatabases(ctx, model.ProjectId, model.InstanceId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, databases []sqlserverflex.Database) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(databases, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex database list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceId, projectLabel string, databases []sqlserverflex.Database) error { + return p.OutputResult(outputFormat, databases, func() error { + if len(databases) == 0 { + p.Outputf("No databases found for instance %s on project %s\n", instanceId, projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(databases, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex database list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME") for i := range databases { @@ -168,5 +145,5 @@ func outputResult(p *print.Printer, outputFormat string, databases []sqlserverfl } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/database/list/list_test.go b/internal/cmd/beta/sqlserverflex/database/list/list_test.go index 0aa4979f7..852236309 100644 --- a/internal/cmd/beta/sqlserverflex/database/list/list_test.go +++ b/internal/cmd/beta/sqlserverflex/database/list/list_test.go @@ -7,21 +7,24 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -53,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiListDatabasesRequest)) sqlserverflex.ApiListDatabasesRequest { - request := testClient.ListDatabases(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.ListDatabases(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +66,7 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiListDatabasesRequest) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -138,48 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -215,6 +178,8 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + instanceId string + projectLabel string databases []sqlserverflex.Database } tests := []struct { @@ -235,11 +200,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.databases); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceId, tt.args.projectLabel, tt.args.databases); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/instance/create/create.go b/internal/cmd/beta/sqlserverflex/instance/create/create.go index b30d7615c..e3a0fe312 100644 --- a/internal/cmd/beta/sqlserverflex/instance/create/create.go +++ b/internal/cmd/beta/sqlserverflex/instance/create/create.go @@ -2,12 +2,11 @@ package create import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,19 +20,19 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api/wait" ) // enforce implementation of interfaces var ( - _ sqlServerFlexClient = &sqlserverflex.APIClient{} + _ sqlServerFlexClient = sqlserverflex.APIClient{}.DefaultAPI ) type sqlServerFlexClient interface { CreateInstance(ctx context.Context, projectId string, region string) sqlserverflex.ApiCreateInstanceRequest - ListFlavorsExecute(ctx context.Context, projectId string, region string) (*sqlserverflex.ListFlavorsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, flavorId string, region string) (*sqlserverflex.ListStoragesResponse, error) + ListFlavors(ctx context.Context, projectId string, region string) sqlserverflex.ApiListFlavorsRequest + ListStorages(ctx context.Context, projectId, flavorId string, region string) sqlserverflex.ApiListStoragesRequest } const ( @@ -53,12 +52,12 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - InstanceName *string - ACL *[]string + InstanceName string + ACL []string BackupSchedule *string FlavorId *string - CPU *int64 - RAM *int64 + CPU *int32 + RAM *int32 StorageClass *string StorageSize *int64 Version *string @@ -66,7 +65,7 @@ type inputModel struct { RetentionDays *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a SQLServer Flex instance", @@ -82,12 +81,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit beta sqlserverflex instance create --name my-instance --flavor-id xxx`), examples.NewExample( `Create a SQLServer Flex instance with name "my-instance", specify flavor by CPU and RAM, set storage size to 20 GB, and restrict access to a specific range of IP addresses. Other parameters are set to default values`, - `$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24`), + `$ stackit beta sqlserverflex instance create --name my-instance --cpu 1 --ram 4 --storage-size 20 --acl 1.2.3.0/24`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -104,16 +103,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a SQLServer Flex instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a SQLServer Flex instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -125,13 +122,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SQLServer Flex instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, resp) @@ -146,8 +143,8 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc.") cmd.Flags().String(backupScheduleFlag, "", "Backup schedule") cmd.Flags().String(flavorIdFlag, "", "ID of the flavor") - cmd.Flags().Int64(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB)") + cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") + cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB)") cmd.Flags().String(storageClassFlag, "", "Storage class") cmd.Flags().String(versionFlag, "", "SQLServer version") @@ -158,15 +155,15 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) - cpu := flags.FlagToInt64Pointer(p, cmd, cpuFlag) - ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) + cpu := flags.FlagToInt32Pointer(p, cmd, cpuFlag) + ram := flags.FlagToInt32Pointer(p, cmd, ramFlag) if flavorId == nil && (cpu == nil || ram == nil) { return nil, &cliErr.DatabaseInputFlavorError{ @@ -183,8 +180,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), - ACL: flags.FlagToStringSlicePointer(p, cmd, aclFlag), + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), + ACL: flags.FlagToStringSliceValue(p, cmd, aclFlag), BackupSchedule: flags.FlagToStringPointer(p, cmd, backupScheduleFlag), FlavorId: flavorId, CPU: cpu, @@ -196,15 +193,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { RetentionDays: flags.FlagToInt64Pointer(p, cmd, retentionDaysFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -214,7 +203,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlServerFle var flavorId *string var err error - flavors, err := apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get SQLServer Flex flavors: %w", err) } @@ -236,7 +225,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlServerFle flavorId = model.FlavorId } - storages, err := apiClient.ListStoragesExecute(ctx, model.ProjectId, *flavorId, model.Region) + storages, err := apiClient.ListStorages(ctx, model.ProjectId, *flavorId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get SQLServer Flex storages: %w", err) } @@ -252,15 +241,15 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlServerFle req = req.CreateInstancePayload(sqlserverflex.CreateInstancePayload{ Name: model.InstanceName, - Acl: &sqlserverflex.CreateInstancePayloadAcl{Items: model.ACL}, + Acl: &sqlserverflex.InstanceDocumentationACL{Items: model.ACL}, BackupSchedule: model.BackupSchedule, - FlavorId: flavorId, - Storage: &sqlserverflex.CreateInstancePayloadStorage{ + FlavorId: *flavorId, + Storage: &sqlserverflex.InstanceDocumentationStorage{ Class: model.StorageClass, Size: model.StorageSize, }, Version: model.Version, - Options: &sqlserverflex.CreateInstancePayloadOptions{ + Options: &sqlserverflex.InstanceDocumentationOptions{ Edition: model.Edition, RetentionDays: retentionDays, }, @@ -272,29 +261,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp if resp == nil { return fmt.Errorf("sqlserverflex response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServerFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServerFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, utils.PtrString(resp.Id)) return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/instance/create/create_test.go b/internal/cmd/beta/sqlserverflex/instance/create/create_test.go index 851d3d1fd..2ba8a7b68 100644 --- a/internal/cmd/beta/sqlserverflex/instance/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/create/create_test.go @@ -8,47 +8,43 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} -var testRegion = "eu01" +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} -// enforce implementation of interfaces -var ( - _ sqlServerFlexClient = &sqlServerFlexClientMocked{} -) +var testRegion = "eu01" -type sqlServerFlexClientMocked struct { +type mockSettings struct { listFlavorsFails bool listFlavorsResp *sqlserverflex.ListFlavorsResponse listStoragesFails bool listStoragesResp *sqlserverflex.ListStoragesResponse } -func (c *sqlServerFlexClientMocked) CreateInstance(ctx context.Context, projectId, region string) sqlserverflex.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId, region) -} - -func (c *sqlServerFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil -} - -func (c *sqlServerFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) sqlserverflex.DefaultAPI { + return &sqlserverflex.DefaultAPIServiceMock{ + ListStoragesExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListStoragesRequest) (*sqlserverflex.ListStoragesResponse, error) { + if s.listFlavorsFails { + return nil, fmt.Errorf("list storages failed") + } + return s.listStoragesResp, nil + }), + ListFlavorsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListFlavorsRequest) (*sqlserverflex.ListFlavorsResponse, error) { + if s.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return s.listFlavorsResp, nil + }), } - return c.listFlavorsResp, nil } var testProjectId = uuid.NewString() @@ -81,8 +77,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), - ACL: utils.Ptr([]string{"0.0.0.0/0"}), + InstanceName: "example-name", + ACL: []string{"0.0.0.0/0"}, BackupSchedule: utils.Ptr("0 0/6 * * *"), FlavorId: utils.Ptr(testFlavorId), StorageClass: utils.Ptr("storage-class"), @@ -98,7 +94,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateInstanceRequest)) sqlserverflex.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion) request = request.CreateInstancePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -108,16 +104,16 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateInstanceRequest func fixturePayload(mods ...func(payload *sqlserverflex.CreateInstancePayload)) sqlserverflex.CreateInstancePayload { payload := sqlserverflex.CreateInstancePayload{ - Name: utils.Ptr("example-name"), - Acl: &sqlserverflex.CreateInstancePayloadAcl{Items: utils.Ptr([]string{"0.0.0.0/0"})}, + Name: "example-name", + Acl: &sqlserverflex.InstanceDocumentationACL{Items: []string{"0.0.0.0/0"}}, BackupSchedule: utils.Ptr("0 0/6 * * *"), - FlavorId: utils.Ptr(testFlavorId), - Storage: &sqlserverflex.CreateInstancePayloadStorage{ + FlavorId: testFlavorId, + Storage: &sqlserverflex.InstanceDocumentationStorage{ Class: utils.Ptr("storage-class"), Size: utils.Ptr(int64(10)), }, Version: utils.Ptr("6.0"), - Options: &sqlserverflex.CreateInstancePayloadOptions{ + Options: &sqlserverflex.InstanceDocumentationOptions{ Edition: utils.Ptr("developer"), RetentionDays: utils.Ptr("32"), }, @@ -131,6 +127,7 @@ func fixturePayload(mods ...func(payload *sqlserverflex.CreateInstancePayload)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -152,8 +149,8 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }), }, { @@ -220,9 +217,7 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.ACL = utils.Ptr( - append(*model.ACL, "198.51.100.14/24", "198.51.100.14/32"), - ) + model.ACL = append(model.ACL, "198.51.100.14/24", "198.51.100.14/32") }), }, { @@ -231,9 +226,7 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.ACL = utils.Ptr( - append(*model.ACL, "198.51.100.14/24", "198.51.100.14/32"), - ) + model.ACL = append(model.ACL, "198.51.100.14/24", "198.51.100.14/32") }), }, { @@ -251,56 +244,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.aclValues { - err := cmd.Flags().Set(aclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", aclFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + aclFlag: tt.aclValues, + }, tt.isValid) }) } } @@ -322,16 +268,16 @@ func TestBuildRequest(t *testing.T) { isValid: true, expectedRequest: fixtureRequest(), listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, listStoragesResp: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"storage-class"}, + StorageClasses: []string{"storage-class"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), @@ -343,28 +289,28 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel( func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), isValid: true, expectedRequest: fixtureRequest(), listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), }, }, }, listStoragesResp: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"storage-class"}, + StorageClasses: []string{"storage-class"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), @@ -376,8 +322,8 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel( func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), listFlavorsFails: true, @@ -388,21 +334,21 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel( func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(5)) - model.RAM = utils.Ptr(int64(9)) + model.CPU = utils.Ptr(int32(5)) + model.RAM = utils.Ptr(int32(9)) }, ), listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), }, }, }, @@ -413,8 +359,8 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel( func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), listFlavorsFails: true, @@ -428,16 +374,16 @@ func TestBuildRequest(t *testing.T) { }, ), listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, listStoragesResp: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"storage-class"}, + StorageClasses: []string{"storage-class"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), @@ -453,16 +399,16 @@ func TestBuildRequest(t *testing.T) { }, ), listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, listStoragesResp: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"storage-class"}, + StorageClasses: []string{"storage-class"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), @@ -474,13 +420,13 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &sqlServerFlexClientMocked{ + client := mockSettings{ listFlavorsFails: tt.listFlavorsFails, listFlavorsResp: tt.listFlavorsResp, listStoragesFails: tt.listStoragesFails, listStoragesResp: tt.listStoragesResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(client)) if err != nil { if !tt.isValid { return @@ -491,6 +437,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -524,11 +473,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/instance/delete/delete.go b/internal/cmd/beta/sqlserverflex/instance/delete/delete.go index ab1742879..77a268d97 100644 --- a/internal/cmd/beta/sqlserverflex/instance/delete/delete.go +++ b/internal/cmd/beta/sqlserverflex/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a SQLServer Flex instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SQLServer Flex instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.InstanceId, model.Region) return req } diff --git a/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go b/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go index ae971f31d..e2bd2ce46 100644 --- a/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/delete/delete_test.go @@ -4,20 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -59,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteInstanceRequest)) sqlserverflex.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -139,54 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -210,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/beta/sqlserverflex/instance/describe/describe.go b/internal/cmd/beta/sqlserverflex/instance/describe/describe.go index d8e3a60c3..bb156eea9 100644 --- a/internal/cmd/beta/sqlserverflex/instance/describe/describe.go +++ b/internal/cmd/beta/sqlserverflex/instance/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a SQLServer Flex instance", @@ -82,20 +81,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region) return req } @@ -103,27 +94,11 @@ func outputResult(p *print.Printer, outputFormat string, instance *sqlserverflex if instance == nil { return fmt.Errorf("instance response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex instance: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { var acls string if instance.Acl != nil && instance.Acl.HasItems() { - aclsArray := *instance.Acl.Items + aclsArray := instance.Acl.Items acls = strings.Join(aclsArray, ",") } @@ -158,5 +133,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *sqlserverflex } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go index 7412418c6..842fe10d0 100644 --- a/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/describe/describe_test.go @@ -7,16 +7,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiGetInstanceRequest)) sqlserverflex.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -138,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -241,11 +197,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/instance/instance.go b/internal/cmd/beta/sqlserverflex/instance/instance.go index 4ed452eb1..9d8784bc7 100644 --- a/internal/cmd/beta/sqlserverflex/instance/instance.go +++ b/internal/cmd/beta/sqlserverflex/instance/instance.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/instance/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for SQLServer Flex instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/beta/sqlserverflex/instance/list/list.go b/internal/cmd/beta/sqlserverflex/instance/list/list.go index 945eb5024..60de6af3a 100644 --- a/internal/cmd/beta/sqlserverflex/instance/list/list.go +++ b/internal/cmd/beta/sqlserverflex/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all SQLServer Flex instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 SQLServer Flex instances`, "$ stackit beta sqlserverflex instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,23 +66,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get SQLServer Flex instances: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + instances := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - instances := *resp.Items // Truncate output if model.Limit != nil && len(instances) > int(*model.Limit) { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,42 +110,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, instances []sqlserverflex.InstanceListInstance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []sqlserverflex.InstanceListInstance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATUS") for i := range instances { @@ -165,5 +142,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []sqlserverfl } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/instance/list/list_test.go b/internal/cmd/beta/sqlserverflex/instance/list/list_test.go index be4b5debe..2059cb2e6 100644 --- a/internal/cmd/beta/sqlserverflex/instance/list/list_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/list/list_test.go @@ -7,20 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -50,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiListInstancesRequest)) sqlserverflex.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +62,7 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiListInstancesRequest) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -114,48 +117,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +141,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +153,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []sqlserverflex.InstanceListInstance } tests := []struct { @@ -211,11 +174,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/instance/update/update.go b/internal/cmd/beta/sqlserverflex/instance/update/update.go index c857aba35..95649cc54 100644 --- a/internal/cmd/beta/sqlserverflex/instance/update/update.go +++ b/internal/cmd/beta/sqlserverflex/instance/update/update.go @@ -2,12 +2,11 @@ package update import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,20 +19,20 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api/wait" ) // enforce implementation of interfaces var ( - _ sqlServerFlexClient = &sqlserverflex.APIClient{} + _ sqlServerFlexClient = sqlserverflex.APIClient{}.DefaultAPI ) type sqlServerFlexClient interface { PartialUpdateInstance(ctx context.Context, projectId, instanceId string, region string) sqlserverflex.ApiPartialUpdateInstanceRequest - GetInstanceExecute(ctx context.Context, projectId, instanceId string, region string) (*sqlserverflex.GetInstanceResponse, error) - ListFlavorsExecute(ctx context.Context, projectId string, region string) (*sqlserverflex.ListFlavorsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, flavorId string, region string) (*sqlserverflex.ListStoragesResponse, error) + GetInstance(ctx context.Context, projectId, instanceId string, region string) sqlserverflex.ApiGetInstanceRequest + ListFlavors(ctx context.Context, projectId string, region string) sqlserverflex.ApiListFlavorsRequest + ListStorages(ctx context.Context, projectId, flavorId string, region string) sqlserverflex.ApiListStoragesRequest } const ( @@ -53,15 +52,15 @@ type inputModel struct { InstanceId string InstanceName *string - ACL *[]string + ACL []string BackupSchedule *string FlavorId *string - CPU *int64 - RAM *int64 + CPU *int32 + RAM *int32 Version *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a SQLServer Flex instance", @@ -89,22 +88,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -116,13 +113,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SQLServer Flex instance update: %w", err) } - s.Stop() } return outputResult(params.Printer, model, instanceLabel, resp) @@ -137,8 +134,8 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "Lists of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().String(backupScheduleFlag, "", "Backup schedule") cmd.Flags().String(flavorIdFlag, "", "ID of the flavor") - cmd.Flags().Int64(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB)") + cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") + cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") cmd.Flags().String(versionFlag, "", "Version") } @@ -152,9 +149,9 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu instanceName := flags.FlagToStringPointer(p, cmd, instanceNameFlag) flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) - cpu := flags.FlagToInt64Pointer(p, cmd, cpuFlag) - ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) - acl := flags.FlagToStringSlicePointer(p, cmd, aclFlag) + cpu := flags.FlagToInt32Pointer(p, cmd, cpuFlag) + ram := flags.FlagToInt32Pointer(p, cmd, ramFlag) + acl := flags.FlagToStringSliceValue(p, cmd, aclFlag) backupSchedule := flags.FlagToStringPointer(p, cmd, backupScheduleFlag) version := flags.FlagToStringPointer(p, cmd, versionFlag) @@ -183,15 +180,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -201,7 +190,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlServerFle var flavorId *string var err error - flavors, err := apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get SQLServer Flex flavors: %w", err) } @@ -210,7 +199,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlServerFle ram := model.RAM cpu := model.CPU if model.RAM == nil || model.CPU == nil { - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.InstanceId, model.Region) + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get SQLServer Flex instance: %w", err) } @@ -237,9 +226,9 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlServerFle flavorId = model.FlavorId } - var payloadAcl *sqlserverflex.CreateInstancePayloadAcl + var payloadAcl *sqlserverflex.InstanceDocumentationACL if model.ACL != nil { - payloadAcl = &sqlserverflex.CreateInstancePayloadAcl{Items: model.ACL} + payloadAcl = &sqlserverflex.InstanceDocumentationACL{Items: model.ACL} } req = req.PartialUpdateInstancePayload(sqlserverflex.PartialUpdateInstancePayload{ @@ -256,29 +245,12 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, res if resp == nil { return fmt.Errorf("instance response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal update SQLServerFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal update SQLServerFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Updated" if model.Async { operationState = "Triggered update of" } p.Info("%s instance %q\n", operationState, instanceLabel) return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/instance/update/update_test.go b/internal/cmd/beta/sqlserverflex/instance/update/update_test.go index 5db0655ba..22578bdec 100644 --- a/internal/cmd/beta/sqlserverflex/instance/update/update_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/update/update_test.go @@ -8,25 +8,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} -var testRegion = "eu01" +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} -// enforce implementation of interfaces -var ( - _ sqlServerFlexClient = &sqlServerFlexClientMocked{} -) +var testRegion = "eu01" -type sqlServerFlexClientMocked struct { +type mockSettings struct { listFlavorsFails bool listFlavorsResp *sqlserverflex.ListFlavorsResponse listStoragesFails bool @@ -35,29 +32,27 @@ type sqlServerFlexClientMocked struct { getInstanceResp *sqlserverflex.GetInstanceResponse } -func (c *sqlServerFlexClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId, region string) sqlserverflex.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId, region) -} - -func (c *sqlServerFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*sqlserverflex.GetInstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") - } - return c.getInstanceResp, nil -} - -func (c *sqlServerFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil -} - -func (c *sqlServerFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(settings mockSettings) sqlserverflex.DefaultAPI { + return &sqlserverflex.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiGetInstanceRequest) (*sqlserverflex.GetInstanceResponse, error) { + if settings.getInstanceFails { + return nil, fmt.Errorf("get instance failed") + } + return settings.getInstanceResp, nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListStoragesRequest) (*sqlserverflex.ListStoragesResponse, error) { + if settings.listFlavorsFails { + return nil, fmt.Errorf("list storages failed") + } + return settings.listStoragesResp, nil + }), + ListFlavorsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListFlavorsRequest) (*sqlserverflex.ListFlavorsResponse, error) { + if settings.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return settings.listFlavorsResp, nil + }), } - return c.listFlavorsResp, nil } var testProjectId = uuid.NewString() @@ -126,7 +121,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { InstanceId: testInstanceId, FlavorId: utils.Ptr(testFlavorId), InstanceName: utils.Ptr("example-name"), - ACL: utils.Ptr([]string{"0.0.0.0/0"}), + ACL: []string{"0.0.0.0/0"}, BackupSchedule: utils.Ptr("0 0 * * *"), Version: utils.Ptr("5.0"), } @@ -137,7 +132,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiPartialUpdateInstanceRequest)) sqlserverflex.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion) request = request.PartialUpdateInstancePayload(sqlserverflex.PartialUpdateInstancePayload{}) for _, mod := range mods { mod(&request) @@ -197,8 +192,8 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureStandardInputModel(func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }), }, { @@ -274,15 +269,15 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, isValid: true, expectedModel: fixtureRequiredInputModel(func(model *inputModel) { - model.ACL = utils.Ptr([]string{"198.51.100.14/24", "198.51.100.14/32"}) + model.ACL = []string{"198.51.100.14/24", "198.51.100.14/32"} }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -324,7 +319,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -369,15 +364,15 @@ func TestBuildRequest(t *testing.T) { }), isValid: true, listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(sqlserverflex.PartialUpdateInstancePayload{ FlavorId: utils.Ptr(testFlavorId), }), @@ -385,20 +380,20 @@ func TestBuildRequest(t *testing.T) { { description: "update flavor from cpu and ram", model: fixtureRequiredInputModel(func(model *inputModel) { - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }), isValid: true, listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(sqlserverflex.PartialUpdateInstancePayload{ FlavorId: utils.Ptr(testFlavorId), }), @@ -407,8 +402,8 @@ func TestBuildRequest(t *testing.T) { description: "get flavors fails", model: fixtureRequiredInputModel( func(model *inputModel) { - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), listFlavorsFails: true, @@ -418,21 +413,21 @@ func TestBuildRequest(t *testing.T) { description: "flavor id not found", model: fixtureRequiredInputModel( func(model *inputModel) { - model.CPU = utils.Ptr(int64(5)) - model.RAM = utils.Ptr(int64(9)) + model.CPU = utils.Ptr(int32(5)) + model.RAM = utils.Ptr(int32(9)) }, ), listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ - Flavors: &[]sqlserverflex.InstanceFlavorEntry{ + Flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), }, }, }, @@ -443,7 +438,7 @@ func TestBuildRequest(t *testing.T) { model: fixtureRequiredInputModel( func(model *inputModel) { model.FlavorId = nil - model.RAM = utils.Ptr(int64(4)) + model.RAM = utils.Ptr(int32(4)) }, ), getInstanceFails: true, @@ -454,8 +449,8 @@ func TestBuildRequest(t *testing.T) { model: fixtureRequiredInputModel( func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), listFlavorsFails: true, @@ -465,7 +460,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &sqlServerFlexClientMocked{ + s := mockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, listFlavorsFails: tt.listFlavorsFails, @@ -473,7 +468,7 @@ func TestBuildRequest(t *testing.T) { listStoragesFails: tt.listStoragesFails, listStoragesResp: tt.listStoragesResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(s)) if err != nil { if !tt.isValid { return @@ -486,6 +481,10 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), + cmp.FilterPath(func(p cmp.Path) bool { + s := p.String() + return s == "ApiService" + }, cmp.Ignore()), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -520,11 +519,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/options/options.go b/internal/cmd/beta/sqlserverflex/options/options.go index d5d62c10b..144be47f3 100644 --- a/internal/cmd/beta/sqlserverflex/options/options.go +++ b/internal/cmd/beta/sqlserverflex/options/options.go @@ -2,11 +2,10 @@ package options import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -16,21 +15,21 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) // enforce implementation of interfaces var ( - _ sqlServerFlexOptionsClient = &sqlserverflex.APIClient{} + _ sqlServerFlexOptionsClient = sqlserverflex.APIClient{}.DefaultAPI ) type sqlServerFlexOptionsClient interface { - ListFlavorsExecute(ctx context.Context, projectId string, region string) (*sqlserverflex.ListFlavorsResponse, error) - ListVersionsExecute(ctx context.Context, projectId string, region string) (*sqlserverflex.ListVersionsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, flavorId string, region string) (*sqlserverflex.ListStoragesResponse, error) - ListRolesExecute(ctx context.Context, projectId string, instanceId string, region string) (*sqlserverflex.ListRolesResponse, error) - ListCollationsExecute(ctx context.Context, projectId string, instanceId string, region string) (*sqlserverflex.ListCollationsResponse, error) - ListCompatibilityExecute(ctx context.Context, projectId string, instanceId string, region string) (*sqlserverflex.ListCompatibilityResponse, error) + ListFlavors(ctx context.Context, projectId string, region string) sqlserverflex.ApiListFlavorsRequest + ListVersions(ctx context.Context, projectId string, region string) sqlserverflex.ApiListVersionsRequest + ListStorages(ctx context.Context, projectId, flavorId string, region string) sqlserverflex.ApiListStoragesRequest + ListRoles(ctx context.Context, projectId string, instanceId string, region string) sqlserverflex.ApiListRolesRequest + ListCollations(ctx context.Context, projectId string, instanceId string, region string) sqlserverflex.ApiListCollationsRequest + ListCompatibility(ctx context.Context, projectId string, instanceId string, region string) sqlserverflex.ApiListCompatibilityRequest } const ( @@ -60,12 +59,12 @@ type inputModel struct { } type options struct { - Flavors *[]sqlserverflex.InstanceFlavorEntry `json:"flavors,omitempty"` - Versions *[]string `json:"versions,omitempty"` - Storages *flavorStorages `json:"flavorStorages,omitempty"` - UserRoles *instanceUserRoles `json:"userRoles,omitempty"` - DBCollations *instanceDBCollations `json:"dbCollations,omitempty"` - DBCompatibilities *instanceDBCompatibilities `json:"dbCompatibilities,omitempty"` + Flavors []sqlserverflex.InstanceFlavorEntry `json:"flavors,omitempty"` + Versions []string `json:"versions,omitempty"` + Storages *flavorStorages `json:"flavorStorages,omitempty"` + UserRoles *instanceUserRoles `json:"userRoles,omitempty"` + DBCollations *instanceDBCollations `json:"dbCollations,omitempty"` + DBCompatibilities *instanceDBCompatibilities `json:"dbCompatibilities,omitempty"` } type flavorStorages struct { @@ -88,7 +87,7 @@ type instanceDBCompatibilities struct { DBCompatibilities []sqlserverflex.MssqlDatabaseCompatibility `json:"dbCompatibilities"` } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", Short: "Lists SQL Server Flex options", @@ -108,9 +107,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List SQL Server Flex user roles and database compatibilities for a given instance. The IDs of existing instances can be obtained by running "$ stackit beta sqlserverflex instance list"`, "$ stackit beta sqlserverflex options --user-roles --db-compatibilities --instance-id "), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -122,7 +121,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient) + err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("get SQL Server Flex options: %w", err) } @@ -145,7 +144,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(instanceIdFlag, "", `The instance ID to show user roles, database collations and database compatibilities for. Only relevant when "--user-roles", "--db-collations" or "--db-compatibilities" is passed`) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) flavors := flags.FlagToBoolValue(p, cmd, flavorsFlag) @@ -190,15 +189,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -212,37 +203,37 @@ func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputM var err error if model.Flavors { - flavors, err = apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err = apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return fmt.Errorf("get SQL Server Flex flavors: %w", err) } } if model.Versions { - versions, err = apiClient.ListVersionsExecute(ctx, model.ProjectId, model.Region) + versions, err = apiClient.ListVersions(ctx, model.ProjectId, model.Region).Execute() if err != nil { return fmt.Errorf("get SQL Server Flex versions: %w", err) } } if model.Storages { - storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, *model.FlavorId, model.Region) + storages, err = apiClient.ListStorages(ctx, model.ProjectId, *model.FlavorId, model.Region).Execute() if err != nil { return fmt.Errorf("get SQL Server Flex storages: %w", err) } } if model.UserRoles { - userRoles, err = apiClient.ListRolesExecute(ctx, model.ProjectId, *model.InstanceId, model.Region) + userRoles, err = apiClient.ListRoles(ctx, model.ProjectId, *model.InstanceId, model.Region).Execute() if err != nil { return fmt.Errorf("get SQL Server Flex user roles: %w", err) } } if model.DBCollations { - dbCollations, err = apiClient.ListCollationsExecute(ctx, model.ProjectId, *model.InstanceId, model.Region) + dbCollations, err = apiClient.ListCollations(ctx, model.ProjectId, *model.InstanceId, model.Region).Execute() if err != nil { return fmt.Errorf("get SQL Server Flex DB collations: %w", err) } } if model.DBCompatibilities { - dbCompatibilities, err = apiClient.ListCompatibilityExecute(ctx, model.ProjectId, *model.InstanceId, model.Region) + dbCompatibilities, err = apiClient.ListCompatibility(ctx, model.ProjectId, *model.InstanceId, model.Region).Execute() if err != nil { return fmt.Errorf("get SQL Server Flex DB compatibilities: %w", err) } @@ -268,71 +259,51 @@ func outputResult(p *print.Printer, model *inputModel, flavors *sqlserverflex.Li if userRoles != nil && model.InstanceId != nil { options.UserRoles = &instanceUserRoles{ InstanceId: *model.InstanceId, - UserRoles: *userRoles.Roles, + UserRoles: userRoles.Roles, } } if dbCollations != nil && model.InstanceId != nil { options.DBCollations = &instanceDBCollations{ InstanceId: *model.InstanceId, - DBCollations: *dbCollations.Collations, + DBCollations: dbCollations.Collations, } } if dbCompatibilities != nil && model.InstanceId != nil { options.DBCompatibilities = &instanceDBCompatibilities{ InstanceId: *model.InstanceId, - DBCompatibilities: *dbCompatibilities.Compatibilities, + DBCompatibilities: dbCompatibilities.Compatibilities, } } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(options, "", " ") - if err != nil { - return fmt.Errorf("marshal SQL Server Flex options: %w", err) + return p.OutputResult(model.OutputFormat, options, func() error { + content := []tables.Table{} + if model.Flavors && len(options.Flavors) != 0 { + content = append(content, buildFlavorsTable(options.Flavors)) } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if model.Versions && len(options.Versions) != 0 { + content = append(content, buildVersionsTable(options.Versions)) + } + if model.Storages && options.Storages.Storages != nil && len(options.Storages.Storages.StorageClasses) != 0 { + content = append(content, buildStoragesTable(*options.Storages.Storages)) + } + if model.UserRoles && len(options.UserRoles.UserRoles) != 0 { + content = append(content, buildUserRoles(options.UserRoles)) + } + if model.DBCompatibilities && len(options.DBCompatibilities.DBCompatibilities) != 0 { + content = append(content, buildDBCompatibilitiesTable(options.DBCompatibilities.DBCompatibilities)) + } + // Rendered at last because table is very long + if model.DBCollations && len(options.DBCollations.DBCollations) != 0 { + content = append(content, buildDBCollationsTable(options.DBCollations.DBCollations)) + } + + err := tables.DisplayTables(p, content) if err != nil { - return fmt.Errorf("marshal SQL Server Flex options: %w", err) + return fmt.Errorf("display output: %w", err) } - p.Outputln(string(details)) return nil - default: - return outputResultAsTable(p, model, options) - } -} - -func outputResultAsTable(p *print.Printer, model *inputModel, options *options) error { - content := []tables.Table{} - if model.Flavors && len(*options.Flavors) != 0 { - content = append(content, buildFlavorsTable(*options.Flavors)) - } - if model.Versions && len(*options.Versions) != 0 { - content = append(content, buildVersionsTable(*options.Versions)) - } - if model.Storages && options.Storages.Storages != nil && len(*options.Storages.Storages.StorageClasses) != 0 { - content = append(content, buildStoragesTable(*options.Storages.Storages)) - } - if model.UserRoles && len(options.UserRoles.UserRoles) != 0 { - content = append(content, buildUserRoles(options.UserRoles)) - } - if model.DBCompatibilities && len(options.DBCompatibilities.DBCompatibilities) != 0 { - content = append(content, buildDBCompatibilitiesTable(options.DBCompatibilities.DBCompatibilities)) - } - // Rendered at last because table is very long - if model.DBCollations && len(options.DBCollations.DBCollations) != 0 { - content = append(content, buildDBCollationsTable(options.DBCollations.DBCollations)) - } - - err := tables.DisplayTables(p, content) - if err != nil { - return fmt.Errorf("display output: %w", err) - } - - return nil + }) } func buildFlavorsTable(flavors []sqlserverflex.InstanceFlavorEntry) tables.Table { @@ -358,7 +329,7 @@ func buildVersionsTable(versions []string) tables.Table { } func buildStoragesTable(storagesResp sqlserverflex.ListStoragesResponse) tables.Table { - storages := *storagesResp.StorageClasses + storages := storagesResp.StorageClasses table := tables.NewTable() table.SetTitle("Storages") table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") diff --git a/internal/cmd/beta/sqlserverflex/options/options_test.go b/internal/cmd/beta/sqlserverflex/options/options_test.go index 1f527d85e..3ac471dd2 100644 --- a/internal/cmd/beta/sqlserverflex/options/options_test.go +++ b/internal/cmd/beta/sqlserverflex/options/options_test.go @@ -5,13 +5,14 @@ import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) type testCtxKey struct{} @@ -19,12 +20,7 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testInstanceId = uuid.NewString() -// enforce implementation of interfaces -var ( - _ sqlServerFlexOptionsClient = &sqlServerFlexClientMocked{} -) - -type sqlServerFlexClientMocked struct { +type mockSettings struct { listFlavorsFails bool listVersionsFails bool listStoragesFails bool @@ -40,68 +36,67 @@ type sqlServerFlexClientMocked struct { listDBCompatibilitiesCalled bool } -func (c *sqlServerFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListFlavorsResponse, error) { - c.listFlavorsCalled = true - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") - } - return utils.Ptr(sqlserverflex.ListFlavorsResponse{ - Flavors: utils.Ptr([]sqlserverflex.InstanceFlavorEntry{}), - }), nil -} - -func (c *sqlServerFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListVersionsResponse, error) { - c.listVersionsCalled = true - if c.listVersionsFails { - return nil, fmt.Errorf("list versions failed") - } - return utils.Ptr(sqlserverflex.ListVersionsResponse{ - Versions: utils.Ptr([]string{}), - }), nil -} - -func (c *sqlServerFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListStoragesResponse, error) { - c.listStoragesCalled = true - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") - } - return utils.Ptr(sqlserverflex.ListStoragesResponse{ - StorageClasses: utils.Ptr([]string{}), - StorageRange: &sqlserverflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }), nil -} - -func (c *sqlServerFlexClientMocked) ListRolesExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListRolesResponse, error) { - c.listUserRolesCalled = true - if c.listUserRolesFails { - return nil, fmt.Errorf("list roles failed") - } - return utils.Ptr(sqlserverflex.ListRolesResponse{ - Roles: utils.Ptr([]string{}), - }), nil -} - -func (c *sqlServerFlexClientMocked) ListCollationsExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListCollationsResponse, error) { - c.listDBCollationsCalled = true - if c.listDBCollationsFails { - return nil, fmt.Errorf("list collations failed") - } - return utils.Ptr(sqlserverflex.ListCollationsResponse{ - Collations: utils.Ptr([]sqlserverflex.MssqlDatabaseCollation{}), - }), nil -} - -func (c *sqlServerFlexClientMocked) ListCompatibilityExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListCompatibilityResponse, error) { - c.listDBCompatibilitiesCalled = true - if c.listDBCompatibilitiesFails { - return nil, fmt.Errorf("list compatibilities failed") +func newAPIMock(s *mockSettings) sqlserverflex.DefaultAPI { + return &sqlserverflex.DefaultAPIServiceMock{ + ListFlavorsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListFlavorsRequest) (*sqlserverflex.ListFlavorsResponse, error) { + s.listFlavorsCalled = true + if s.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return utils.Ptr(sqlserverflex.ListFlavorsResponse{ + Flavors: []sqlserverflex.InstanceFlavorEntry{}, + }), nil + }), + ListVersionsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListVersionsRequest) (*sqlserverflex.ListVersionsResponse, error) { + s.listVersionsCalled = true + if s.listVersionsFails { + return nil, fmt.Errorf("list versions failed") + } + return utils.Ptr(sqlserverflex.ListVersionsResponse{ + Versions: []string{}, + }), nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListStoragesRequest) (*sqlserverflex.ListStoragesResponse, error) { + s.listStoragesCalled = true + if s.listStoragesFails { + return nil, fmt.Errorf("list storages failed") + } + return utils.Ptr(sqlserverflex.ListStoragesResponse{ + StorageClasses: []string{}, + StorageRange: &sqlserverflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, + }), nil + }), + ListRolesExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListRolesRequest) (*sqlserverflex.ListRolesResponse, error) { + s.listUserRolesCalled = true + if s.listUserRolesFails { + return nil, fmt.Errorf("list roles failed") + } + return utils.Ptr(sqlserverflex.ListRolesResponse{ + Roles: []string{}, + }), nil + }), + ListCollationsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListCollationsRequest) (*sqlserverflex.ListCollationsResponse, error) { + s.listDBCollationsCalled = true + if s.listDBCollationsFails { + return nil, fmt.Errorf("list collations failed") + } + return utils.Ptr(sqlserverflex.ListCollationsResponse{ + Collations: []sqlserverflex.MssqlDatabaseCollation{}, + }), nil + }), + ListCompatibilityExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListCompatibilityRequest) (*sqlserverflex.ListCompatibilityResponse, error) { + s.listDBCompatibilitiesCalled = true + if s.listDBCompatibilitiesFails { + return nil, fmt.Errorf("list compatibilities failed") + } + return utils.Ptr(sqlserverflex.ListCompatibilityResponse{ + Compatibilities: []sqlserverflex.MssqlDatabaseCompatibility{}, + }), nil + }), } - return utils.Ptr(sqlserverflex.ListCompatibilityResponse{ - Compatibilities: utils.Ptr([]sqlserverflex.MssqlDatabaseCompatibility{}), - }), nil } func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -158,6 +153,7 @@ func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -263,46 +259,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -472,10 +429,8 @@ func TestBuildAndExecuteRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - p.Cmd = cmd - client := &sqlServerFlexClientMocked{ + params := testparams.NewTestParams() + settings := mockSettings{ listFlavorsFails: tt.listFlavorsFails, listVersionsFails: tt.listVersionsFails, listStoragesFails: tt.listStoragesFails, @@ -484,7 +439,7 @@ func TestBuildAndExecuteRequest(t *testing.T) { listDBCompatibilitiesFails: tt.listDBCompatibilitiesFails, } - err := buildAndExecuteRequest(testCtx, p, tt.model, client) + err := buildAndExecuteRequest(testCtx, params.Printer, tt.model, newAPIMock(&settings)) if err != nil && tt.isValid { t.Fatalf("error building and executing request: %v", err) } @@ -495,14 +450,14 @@ func TestBuildAndExecuteRequest(t *testing.T) { return } - if tt.expectListFlavorsCalled != client.listFlavorsCalled { - t.Fatalf("expected listFlavorsCalled to be %v, got %v", tt.expectListFlavorsCalled, client.listFlavorsCalled) + if tt.expectListFlavorsCalled != settings.listFlavorsCalled { + t.Fatalf("expected listFlavorsCalled to be %v, got %v", tt.expectListFlavorsCalled, settings.listFlavorsCalled) } - if tt.expectListVersionsCalled != client.listVersionsCalled { - t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, client.listVersionsCalled) + if tt.expectListVersionsCalled != settings.listVersionsCalled { + t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, settings.listVersionsCalled) } - if tt.expectListStoragesCalled != client.listStoragesCalled { - t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, client.listStoragesCalled) + if tt.expectListStoragesCalled != settings.listStoragesCalled { + t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, settings.listStoragesCalled) } }) } @@ -534,21 +489,21 @@ func TestOutputResult(t *testing.T) { name: "all input set", args: args{ model: fixtureInputModelAllTrue(), - flavors: &sqlserverflex.ListFlavorsResponse{Flavors: &[]sqlserverflex.InstanceFlavorEntry{}}, - versions: &sqlserverflex.ListVersionsResponse{Versions: &[]string{}}, - storages: &sqlserverflex.ListStoragesResponse{StorageClasses: &[]string{}}, - userRoles: &sqlserverflex.ListRolesResponse{Roles: &[]string{}}, - dbCollations: &sqlserverflex.ListCollationsResponse{Collations: &[]sqlserverflex.MssqlDatabaseCollation{}}, - dbCompatibilities: &sqlserverflex.ListCompatibilityResponse{Compatibilities: &[]sqlserverflex.MssqlDatabaseCompatibility{}}, + flavors: &sqlserverflex.ListFlavorsResponse{Flavors: []sqlserverflex.InstanceFlavorEntry{}}, + versions: &sqlserverflex.ListVersionsResponse{Versions: []string{}}, + storages: &sqlserverflex.ListStoragesResponse{StorageClasses: []string{}}, + userRoles: &sqlserverflex.ListRolesResponse{Roles: []string{}}, + dbCollations: &sqlserverflex.ListCollationsResponse{Collations: []sqlserverflex.MssqlDatabaseCollation{}}, + dbCompatibilities: &sqlserverflex.ListCompatibilityResponse{Compatibilities: []sqlserverflex.MssqlDatabaseCompatibility{}}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.flavors, tt.args.versions, tt.args.storages, tt.args.userRoles, tt.args.dbCollations, tt.args.dbCompatibilities); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.flavors, tt.args.versions, tt.args.storages, tt.args.userRoles, tt.args.dbCollations, tt.args.dbCompatibilities); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/sqlserverflex.go b/internal/cmd/beta/sqlserverflex/sqlserverflex.go index 51c93beb8..29404a045 100644 --- a/internal/cmd/beta/sqlserverflex/sqlserverflex.go +++ b/internal/cmd/beta/sqlserverflex/sqlserverflex.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/options" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "sqlserverflex", Short: "Provides functionality for SQLServer Flex", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(database.NewCmd(params)) cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(options.NewCmd(params)) diff --git a/internal/cmd/beta/sqlserverflex/user/create/create.go b/internal/cmd/beta/sqlserverflex/user/create/create.go index 7cd803ec7..22957b824 100644 --- a/internal/cmd/beta/sqlserverflex/user/create/create.go +++ b/internal/cmd/beta/sqlserverflex/user/create/create.go @@ -2,13 +2,14 @@ package create import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/client" sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) const ( @@ -31,11 +31,11 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - Username *string - Roles *[]string + Username string + Roles []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a SQLServer Flex user", @@ -44,7 +44,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "The password is only visible upon creation and cannot be retrieved later.", "Alternatively, you can reset the password and access the new one by running:", " $ stackit beta sqlserverflex user reset-password USER_ID --instance-id INSTANCE_ID", - "Please refer to https://docs.stackit.cloud/stackit/en/creating-logins-and-users-in-sqlserver-flex-instances-210862358.html for additional information.", + "Please refer to https://docs.stackit.cloud/products/databases/sqlserver-flex/how-tos/create-logins-and-users-in-sqlserver-flex-instances/ for additional information.", "The allowed user roles for your instance can be obtained by running:", " $ stackit beta sqlserverflex options --user-roles --instance-id INSTANCE_ID", ), @@ -57,9 +57,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit beta sqlserverflex user create --instance-id xxx --username johndoe --roles "##STACKIT_LoginManager##,##STACKIT_DatabaseManager##"`), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,18 +70,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -109,7 +107,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,24 +116,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), - Username: flags.FlagToStringPointer(p, cmd, usernameFlag), - Roles: flags.FlagToStringSlicePointer(p, cmd, rolesFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Username: flags.FlagToStringValue(p, cmd, usernameFlag), + Roles: flags.FlagToStringSliceValue(p, cmd, rolesFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiCreateUserRequest { - req := apiClient.CreateUser(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.CreateUser(ctx, model.ProjectId, model.InstanceId, model.Region) req = req.CreateUserPayload(sqlserverflex.CreateUserPayload{ Username: model.Username, @@ -148,29 +138,12 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, use if user == nil { return fmt.Errorf("user response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, user, func() error { p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, utils.PtrString(user.Id)) p.Outputf("Username: %s\n", utils.PtrString(user.Username)) p.Outputf("Password: %s\n", utils.PtrString(user.Password)) - if user.Roles != nil && len(*user.Roles) != 0 { - p.Outputf("Roles: [%v]\n", strings.Join(*user.Roles, ", ")) + if len(user.Roles) != 0 { + p.Outputf("Roles: [%v]\n", strings.Join(user.Roles, ", ")) } if user.Host != nil && *user.Host != "" { p.Outputf("Host: %s\n", *user.Host) @@ -183,5 +156,5 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, use } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/user/create/create_test.go b/internal/cmd/beta/sqlserverflex/user/create/create_test.go index 4c294d6d9..0a62cd242 100644 --- a/internal/cmd/beta/sqlserverflex/user/create/create_test.go +++ b/internal/cmd/beta/sqlserverflex/user/create/create_test.go @@ -7,18 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -45,8 +45,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - Username: utils.Ptr("johndoe"), - Roles: utils.Ptr([]string{"read"}), + Username: "johndoe", + Roles: []string{"read"}, } for _, mod := range mods { mod(model) @@ -55,10 +55,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateUserRequest)) sqlserverflex.ApiCreateUserRequest { - request := testClient.CreateUser(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.CreateUser(testCtx, testProjectId, testInstanceId, testRegion) request = request.CreateUserPayload(sqlserverflex.CreateUserPayload{ - Username: utils.Ptr("johndoe"), - Roles: utils.Ptr([]string{"read"}), + Username: "johndoe", + Roles: []string{"read"}, }) for _, mod := range mods { @@ -70,6 +70,7 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiCreateUserRequest)) s func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -139,48 +140,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,10 +159,10 @@ func TestBuildRequest(t *testing.T) { { description: "no username specified", model: fixtureInputModel(func(model *inputModel) { - model.Username = nil + model.Username = "" }), expectedRequest: fixtureRequest().CreateUserPayload(sqlserverflex.CreateUserPayload{ - Roles: utils.Ptr([]string{"read"}), + Roles: []string{"read"}, }), }, } @@ -213,7 +173,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -247,11 +207,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/user/delete/delete.go b/internal/cmd/beta/sqlserverflex/user/delete/delete.go index 8250b1b01..317656bec 100644 --- a/internal/cmd/beta/sqlserverflex/user/delete/delete.go +++ b/internal/cmd/beta/sqlserverflex/user/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", userIdArg), Short: "Deletes a SQLServer Flex user", @@ -58,24 +59,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId, model.Region) + userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -114,19 +113,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiDeleteUserRequest { - req := apiClient.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) return req } diff --git a/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go b/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go index c4345ba6a..85c35144b 100644 --- a/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go +++ b/internal/cmd/beta/sqlserverflex/user/delete/delete_test.go @@ -4,20 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "my-user-id" @@ -62,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiDeleteUserRequest)) sqlserverflex.ApiDeleteUserRequest { - request := testClient.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) for _, mod := range mods { mod(&request) } @@ -154,54 +154,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -225,7 +178,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/beta/sqlserverflex/user/describe/describe.go b/internal/cmd/beta/sqlserverflex/user/describe/describe.go index 1d94890ac..e09ce9d80 100644 --- a/internal/cmd/beta/sqlserverflex/user/describe/describe.go +++ b/internal/cmd/beta/sqlserverflex/user/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) const ( @@ -35,7 +34,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", userIdArg), Short: "Shows details of a SQLServer Flex user", @@ -102,20 +101,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiGetUserRequest { - req := apiClient.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) return req } @@ -123,31 +114,15 @@ func outputResult(p *print.Printer, outputFormat string, user *sqlserverflex.Use if user == nil { return fmt.Errorf("user response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex user: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, user, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(user.Id)) table.AddSeparator() table.AddRow("USERNAME", utils.PtrString(user.Username)) - if user.Roles != nil && len(*user.Roles) != 0 { + if len(user.Roles) != 0 { table.AddSeparator() - table.AddRow("ROLES", strings.Join(*user.Roles, "\n")) + table.AddRow("ROLES", strings.Join(user.Roles, "\n")) } if user.DefaultDatabase != nil && *user.DefaultDatabase != "" { table.AddSeparator() @@ -168,5 +143,5 @@ func outputResult(p *print.Printer, outputFormat string, user *sqlserverflex.Use } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go b/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go index 8f44c6db7..e07a2d3c3 100644 --- a/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go +++ b/internal/cmd/beta/sqlserverflex/user/describe/describe_test.go @@ -7,16 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "my-user-id" @@ -61,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiGetUserRequest)) sqlserverflex.ApiGetUserRequest { - request := testClient.GetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.GetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) for _, mod := range mods { mod(&request) } @@ -153,54 +155,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -224,7 +179,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -256,11 +211,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/user/list/list.go b/internal/cmd/beta/sqlserverflex/user/list/list.go index c45317bf2..9845eb3b0 100644 --- a/internal/cmd/beta/sqlserverflex/user/list/list.go +++ b/internal/cmd/beta/sqlserverflex/user/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all SQLServer Flex users of an instance", @@ -50,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit beta sqlserverflex user list --instance-id xxx --limit 10"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,23 +69,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get SQLServer Flex users: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId, model.Region) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = *model.InstanceId - } - params.Printer.Info("No users found for instance %q\n", instanceLabel) - return nil + users := resp.GetItems() + + instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, *model.InstanceId, model.Region) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = *model.InstanceId } - users := *resp.Items // Truncate output if model.Limit != nil && len(users) > int(*model.Limit) { users = users[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, users) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, users) }, } @@ -101,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -121,42 +118,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiListUsersRequest { - req := apiClient.ListUsers(ctx, model.ProjectId, *model.InstanceId, model.Region) + req := apiClient.DefaultAPI.ListUsers(ctx, model.ProjectId, *model.InstanceId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, users []sqlserverflex.InstanceListUser) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(users, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex user list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, users []sqlserverflex.InstanceListUser) error { + return p.OutputResult(outputFormat, users, func() error { + if len(users) == 0 { + p.Outputf("No users found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex user list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "USERNAME") for i := range users { @@ -172,5 +149,5 @@ func outputResult(p *print.Printer, outputFormat string, users []sqlserverflex.I } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/user/list/list_test.go b/internal/cmd/beta/sqlserverflex/user/list/list_test.go index 7ec60722f..e4d6b8ce2 100644 --- a/internal/cmd/beta/sqlserverflex/user/list/list_test.go +++ b/internal/cmd/beta/sqlserverflex/user/list/list_test.go @@ -7,21 +7,23 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -53,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiListUsersRequest)) sqlserverflex.ApiListUsersRequest { - request := testClient.ListUsers(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.ListUsers(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +65,7 @@ func fixtureRequest(mods ...func(request *sqlserverflex.ApiListUsersRequest)) sq func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -131,48 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -196,7 +158,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -207,8 +169,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - users []sqlserverflex.InstanceListUser + outputFormat string + instanceLabel string + users []sqlserverflex.InstanceListUser } tests := []struct { name string @@ -228,11 +191,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.users); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.users); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password.go b/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password.go index 35ef24063..55c5df301 100644 --- a/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password.go +++ b/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password.go @@ -2,11 +2,10 @@ package resetpassword import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("reset-password %s", userIdArg), Short: "Resets the password of a SQLServer Flex user", @@ -61,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := sqlserverflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId, model.Region) + userLabel, err := sqlserverflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -117,20 +114,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *sqlserverflex.APIClient) sqlserverflex.ApiResetUserRequest { - req := apiClient.ResetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.ResetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) return req } @@ -138,24 +127,8 @@ func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel strin if user == nil { return fmt.Errorf("single user response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal SQLServer Flex reset password: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SQLServer Flex reset password: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, user, func() error { p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel) p.Outputf("Username: %s\n", utils.PtrString(user.Username)) p.Outputf("New password: %s\n", utils.PtrString(user.Password)) @@ -163,5 +136,5 @@ func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel strin p.Outputf("New URI: %s\n", *user.Uri) } return nil - } + }) } diff --git a/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go b/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go index eeeb230a1..eb6569710 100644 --- a/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go +++ b/internal/cmd/beta/sqlserverflex/user/reset-password/reset_password_test.go @@ -7,16 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &sqlserverflex.APIClient{} +var testClient = &sqlserverflex.APIClient{DefaultAPI: &sqlserverflex.DefaultAPIService{}} + var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "my-user-id" @@ -61,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *sqlserverflex.ApiResetUserRequest)) sqlserverflex.ApiResetUserRequest { - request := testClient.ResetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.ResetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) for _, mod := range mods { mod(&request) } @@ -153,54 +155,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -224,7 +179,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, sqlserverflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -258,11 +213,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.userLabel, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.userLabel, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/beta/sqlserverflex/user/user.go b/internal/cmd/beta/sqlserverflex/user/user.go index 0c83d54e4..572a9ea52 100644 --- a/internal/cmd/beta/sqlserverflex/user/user.go +++ b/internal/cmd/beta/sqlserverflex/user/user.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/list" resetpassword "github.com/stackitcloud/stackit-cli/internal/cmd/beta/sqlserverflex/user/reset-password" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "user", Short: "Provides functionality for SQLServer Flex users", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/beta/vpn/connection/connection.go b/internal/cmd/beta/vpn/connection/connection.go new file mode 100644 index 000000000..2de835d71 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/connection.go @@ -0,0 +1,34 @@ +package connection + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection/status" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "connection", + Short: "Provides functionality for VPN connections", + Long: "Provides functionality for VPN connections.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, p) + return cmd +} + +func addSubcommands(cmd *cobra.Command, p *types.CmdParams) { + cmd.AddCommand(create.NewCmd(p)) + cmd.AddCommand(delete.NewCmd(p)) + cmd.AddCommand(describe.NewCmd(p)) + cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(status.NewCmd(p)) +} diff --git a/internal/cmd/beta/vpn/connection/create/create.go b/internal/cmd/beta/vpn/connection/create/create.go new file mode 100644 index 000000000..248c97b30 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/create/create.go @@ -0,0 +1,393 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + gatewayIdFlag = "gateway-id" + + displayNameFlag = "display-name" + enabledFlag = "enabled" + labelsFlag = "labels" + localSubnetsFlag = "local-subnets" + remoteSubnetsFlag = "remote-subnets" + staticRoutesFlag = "static-routes" + + tunnel1BgpRemoteAsnFlag = "tunnel1-bgp-remote-asn" + tunnel1PeeringLocalAddressFlag = "tunnel1-peering-local-address" + tunnel1PeeringRemoteAddressFlag = "tunnel1-peering-remote-address" + tunnel1Phase1RekeyTimeFlag = "tunnel1-phase1-rekey-time" + tunnel1Phase2RekeyTimeFlag = "tunnel1-phase2-rekey-time" + tunnel1PreSharedKeyFlag = "tunnel1-pre-shared-key" + tunnel1RemoteAddressFlag = "tunnel1-remote-address" + + tunnel2BgpRemoteAsnFlag = "tunnel2-bgp-remote-asn" + tunnel2PeeringLocalAddressFlag = "tunnel2-peering-local-address" + tunnel2PeeringRemoteAddressFlag = "tunnel2-peering-remote-address" + tunnel2Phase1RekeyTimeFlag = "tunnel2-phase1-rekey-time" + tunnel2Phase2RekeyTimeFlag = "tunnel2-phase2-rekey-time" + tunnel2PreSharedKeyFlag = "tunnel2-pre-shared-key" + tunnel2RemoteAddressFlag = "tunnel2-remote-address" +) + +var ( + // tunnel 1 + tunnel1Phase1DhGroupsFlag = flags.StringEnumSliceFlag( + "tunnel1-phase1-dh-groups", + vpn.AllowedPhaseDhGroupsInnerEnumValues, + "Tunnel 1 Phase 1 DH Groups.\nThe Diffie-Hellman Group. Required, except if AEAD algorithms are selected.", + ) + tunnel1Phase1EncryptionAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel1-phase1-encryption-algorithms", + vpn.AllowedPhaseEncryptionAlgorithmsInnerEnumValues, + "Required: Tunnel 1 Phase 1 Encryption Algorithms", + ) + tunnel1Phase1IntegrityAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel1-phase1-integrity-algorithms", + vpn.AllowedPhaseIntegrityAlgorithmsInnerEnumValues, + "Required: Tunnel 1 Phase 1 Integrity Algorithms", + ) + tunnel1Phase2DhGroupsFlag = flags.StringEnumSliceFlag( + "tunnel1-phase2-dh-groups", + vpn.AllowedPhaseDhGroupsInnerEnumValues, + "Tunnel 1 Phase 2 DH Groups", + ) + tunnel1Phase2EncryptionAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel1-phase2-encryption-algorithms", + vpn.AllowedPhaseEncryptionAlgorithmsInnerEnumValues, + "Required: Tunnel 1 Phase 2 Encryption Algorithms", + ) + tunnel1Phase2IntegrityAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel1-phase2-integrity-algorithms", + vpn.AllowedPhaseIntegrityAlgorithmsInnerEnumValues, + "Required: Tunnel 1 Phase 2 Integrity Algorithms", + ) + tunnel1Phase2DpdActionFlag = flags.StringEnumFlag( + "tunnel1-phase2-dpd-action", + vpn.AllowedTunnelConfigurationPhase2AllOfDpdActionEnumValues, + "Tunnel 1 Phase 2 DPD Action.\nAction to perform for this CHILD_SA on DPD timeout. \"clear\": Closes the CHILD_SA and does not take further action. \"restart\": immediately tries to re-negotiate the CILD_SA under a fresh IKE_SA.", + ) + tunnel1Phase2StartActionFlag = flags.StringEnumFlag( + "tunnel1-phase2-start-action", + vpn.AllowedTunnelConfigurationPhase2AllOfStartActionEnumValues, + "Tunnel 1 Phase 2 Start Action.\nAction to perform after loading the connection configuration. \"none\": The connection will be loaded but needs to be manually initiated. \"start\": initiates the connection actively.", + ) + // tunnel 2 + tunnel2Phase1DhGroupsFlag = flags.StringEnumSliceFlag( + "tunnel2-phase1-dh-groups", + vpn.AllowedPhaseDhGroupsInnerEnumValues, + "Tunnel 2 Phase 1 DH Groups\nThe Diffie-Hellman Group. Required, except if AEAD algorithms are selected.", + ) + tunnel2Phase1EncryptionAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel2-phase1-encryption-algorithms", + vpn.AllowedPhaseEncryptionAlgorithmsInnerEnumValues, + "Required: Tunnel 2 Phase 1 Encryption Algorithms", + ) + tunnel2Phase1IntegrityAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel2-phase1-integrity-algorithms", + vpn.AllowedPhaseIntegrityAlgorithmsInnerEnumValues, + "Required: Tunnel 2 Phase 1 Integrity Algorithms", + ) + tunnel2Phase2DhGroupsFlag = flags.StringEnumSliceFlag( + "tunnel2-phase2-dh-groups", + vpn.AllowedPhaseDhGroupsInnerEnumValues, + "Tunnel 2 Phase 2 DH Groups", + ) + tunnel2Phase2EncryptionAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel2-phase2-encryption-algorithms", + vpn.AllowedPhaseEncryptionAlgorithmsInnerEnumValues, + "Required: Tunnel 2 Phase 2 Encryption Algorithms", + ) + tunnel2Phase2IntegrityAlgorithmsFlag = flags.StringEnumSliceFlag( + "tunnel2-phase2-integrity-algorithms", + vpn.AllowedPhaseIntegrityAlgorithmsInnerEnumValues, + "Required: Tunnel 2 Phase 2 Integrity Algorithms", + ) + tunnel2Phase2DpdActionFlag = flags.StringEnumFlag( + "tunnel2-phase2-dpd-action", + vpn.AllowedTunnelConfigurationPhase2AllOfDpdActionEnumValues, + "Tunnel 2 Phase 2 DPD Action.\nAction to perform for this CHILD_SA on DPD timeout. \"clear\": Closes the CHILD_SA and does not take further action. \"restart\": immediately tries to re-negotiate the CILD_SA under a fresh IKE_SA.", + ) + tunnel2Phase2StartActionFlag = flags.StringEnumFlag( + "tunnel2-phase2-start-action", + vpn.AllowedTunnelConfigurationPhase2AllOfStartActionEnumValues, + "Tunnel 2 Phase 2 Start Action.\nDefault: \"start\"\nEnum: \"none\" \"start\"\nAction to perform after loading the connection configuration. \"none\": The connection will be loaded but needs to be manually initiated. \"start\": initiates the connection actively.", + ) +) + +type tunnelInputModel struct { + BgpRemoteAsn *int64 + PeeringLocalAddress *string + PeeringRemoteAddress *string + Phase1DhGroups []vpn.PhaseDhGroupsInner + Phase1EncryptionAlgorithms []vpn.PhaseEncryptionAlgorithmsInner + Phase1IntegrityAlgorithms []vpn.PhaseIntegrityAlgorithmsInner + Phase1RekeyTime *int32 + Phase2DhGroups []vpn.PhaseDhGroupsInner + Phase2EncryptionAlgorithms []vpn.PhaseEncryptionAlgorithmsInner + Phase2IntegrityAlgorithms []vpn.PhaseIntegrityAlgorithmsInner + Phase2RekeyTime *int32 + Phase2DpdAction *vpn.TunnelConfigurationPhase2AllOfDpdAction + Phase2StartAction *vpn.TunnelConfigurationPhase2AllOfStartAction + PreSharedKey string + RemoteAddress string +} + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId string + + DisplayName string + Enabled *bool + Labels *map[string]string + LocalSubnets []string + RemoteSubnets []string + StaticRoutes []string + + Tunnel1 tunnelInputModel + Tunnel2 tunnelInputModel +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a VPN connection", + Long: "Creates a VPN connection.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a VPN connection`, + "$ stackit beta vpn connection create --gateway-id xxx --display-name my-connection --tunnel1-remote-address 1.2.3.4 --tunnel2-remote-address 5.6.7.8"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a VPN connection for gateway %q?", model.GatewayId) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create VPN connection: %w", err) + } + + return outputResult(p.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), gatewayIdFlag, "Required: Gateway ID") + cmd.Flags().String(displayNameFlag, "", "Required: A user friendly name for the connection.") + cmd.Flags().Bool(enabledFlag, true, "Enable the connection") + cmd.Flags().StringToString(labelsFlag, nil, "Map of custom labels. Key and values must be a string with max 63 chars, start/end with alphanumeric. The key of a label follows the same rules as the LabelValue except that it cannot be empty. (example: foo=bar)") + cmd.Flags().StringSlice(localSubnetsFlag, nil, "Defaults to 0.0.0.0/0 for Route-based VPN configurations. Mandatory for Policy-based.") + cmd.Flags().StringSlice(remoteSubnetsFlag, nil, "Defaults to 0.0.0.0/0 for Route-based VPN configurations. Mandatory for Policy-based.") + cmd.Flags().StringSlice(staticRoutesFlag, nil, "Use this for route-based VPN.") + + cmd.Flags().Int64(tunnel1BgpRemoteAsnFlag, 0, "Required: Tunnel 1 BGP Remote ASN.\nASN for private use (reserved by IANA), both 16Bit and 32Bit ranges are valid (RFC 6996).") + cmd.Flags().String(tunnel1PeeringLocalAddressFlag, "", "Tunnel 1 Peering Local Address.\nThe peering object defines the point-to-point IP configuration for the Tunnel Interface. These addresses serve as next-hop identifiers and are used for BGP peering sessions and can be used in Static Route-Based connectivity.") + cmd.Flags().String(tunnel1PeeringRemoteAddressFlag, "", "Tunnel 1 Peering Remote Address") + tunnel1Phase1DhGroupsFlag.Register(cmd) + tunnel1Phase1EncryptionAlgorithmsFlag.Register(cmd) + tunnel1Phase1IntegrityAlgorithmsFlag.Register(cmd) + cmd.Flags().Int64(tunnel1Phase1RekeyTimeFlag, 0, "Tunnel 1 Phase 1 Rekey Time.\nTime to schedule a IKE re-keying (in seconds).") + tunnel1Phase2DhGroupsFlag.Register(cmd) + tunnel1Phase2EncryptionAlgorithmsFlag.Register(cmd) + tunnel1Phase2IntegrityAlgorithmsFlag.Register(cmd) + cmd.Flags().Int64(tunnel1Phase2RekeyTimeFlag, 0, "Tunnel 1 Phase 2 Rekey Time.\nTime to schedule a Child SA re-keying (in seconds).") + tunnel1Phase2DpdActionFlag.Register(cmd.Flags()) + tunnel1Phase2StartActionFlag.Register(cmd.Flags()) + cmd.Flags().String(tunnel1PreSharedKeyFlag, "", "Required: Tunnel 1 Pre Shared Key.\nA Pre-Shared Key for authentication. Required in create-requests, optional in update-requests and omitted in every response.") + cmd.Flags().String(tunnel1RemoteAddressFlag, "", "Tunnel 1 Remote Address") + + cmd.Flags().Int64(tunnel2BgpRemoteAsnFlag, 0, "Tunnel 2 BGP Remote ASN") + cmd.Flags().String(tunnel2PeeringLocalAddressFlag, "", "Tunnel 2 Peering Local Address.\nThe peering object defines the point-to-point IP configuration for the Tunnel Interface. These addresses serve as next-hop identifiers and are used for BGP peering sessions and can be used in Static Route-Based connectivity.") + cmd.Flags().String(tunnel2PeeringRemoteAddressFlag, "", "Tunnel 2 Peering Remote Address") + tunnel2Phase1DhGroupsFlag.Register(cmd) + tunnel2Phase1EncryptionAlgorithmsFlag.Register(cmd) + tunnel2Phase1IntegrityAlgorithmsFlag.Register(cmd) + cmd.Flags().Int64(tunnel2Phase1RekeyTimeFlag, 0, "Tunnel 2 Phase 1 Rekey Time.\nTime to schedule a IKE re-keying (in seconds).") + tunnel2Phase2DhGroupsFlag.Register(cmd) + tunnel2Phase2EncryptionAlgorithmsFlag.Register(cmd) + tunnel2Phase2IntegrityAlgorithmsFlag.Register(cmd) + cmd.Flags().Int64(tunnel2Phase2RekeyTimeFlag, 0, "Tunnel 2 Phase 2 Rekey Time.\nTime to schedule a Child SA re-keying (in seconds).") + tunnel2Phase2DpdActionFlag.Register(cmd.Flags()) + tunnel2Phase2StartActionFlag.Register(cmd.Flags()) + cmd.Flags().String(tunnel2PreSharedKeyFlag, "", "Required: Tunnel 2 Pre Shared Key.\nA Pre-Shared Key for authentication. Required in create-requests, optional in update-requests and omitted in every response.") + cmd.Flags().String(tunnel2RemoteAddressFlag, "", "Tunnel 2 Remote Address") + + err := flags.MarkFlagsRequired( + cmd, + gatewayIdFlag, displayNameFlag, + tunnel1RemoteAddressFlag, + tunnel1PreSharedKeyFlag, + tunnel1Phase1EncryptionAlgorithmsFlag.Name(), tunnel1Phase1IntegrityAlgorithmsFlag.Name(), + tunnel1Phase2EncryptionAlgorithmsFlag.Name(), tunnel1Phase2IntegrityAlgorithmsFlag.Name(), + tunnel2RemoteAddressFlag, + tunnel2PreSharedKeyFlag, + tunnel2Phase1EncryptionAlgorithmsFlag.Name(), tunnel2Phase1IntegrityAlgorithmsFlag.Name(), + tunnel2Phase2EncryptionAlgorithmsFlag.Name(), tunnel2Phase2IntegrityAlgorithmsFlag.Name(), + ) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: flags.FlagToStringValue(p, cmd, gatewayIdFlag), + + DisplayName: flags.FlagToStringValue(p, cmd, displayNameFlag), + Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag), + Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + LocalSubnets: flags.FlagToStringSliceValue(p, cmd, localSubnetsFlag), + RemoteSubnets: flags.FlagToStringSliceValue(p, cmd, remoteSubnetsFlag), + StaticRoutes: flags.FlagToStringSliceValue(p, cmd, staticRoutesFlag), + + Tunnel1: tunnelInputModel{ + BgpRemoteAsn: flags.FlagToInt64Pointer(p, cmd, tunnel1BgpRemoteAsnFlag), + PeeringLocalAddress: flags.FlagToStringPointer(p, cmd, tunnel1PeeringLocalAddressFlag), + PeeringRemoteAddress: flags.FlagToStringPointer(p, cmd, tunnel1PeeringRemoteAddressFlag), + Phase1DhGroups: tunnel1Phase1DhGroupsFlag.Get(), + Phase1EncryptionAlgorithms: tunnel1Phase1EncryptionAlgorithmsFlag.Get(), + Phase1IntegrityAlgorithms: tunnel1Phase1IntegrityAlgorithmsFlag.Get(), + Phase1RekeyTime: flags.FlagToInt32Pointer(p, cmd, tunnel1Phase1RekeyTimeFlag), + Phase2DhGroups: tunnel1Phase2DhGroupsFlag.Get(), + Phase2EncryptionAlgorithms: tunnel1Phase2EncryptionAlgorithmsFlag.Get(), + Phase2IntegrityAlgorithms: tunnel1Phase2IntegrityAlgorithmsFlag.Get(), + Phase2RekeyTime: flags.FlagToInt32Pointer(p, cmd, tunnel1Phase2RekeyTimeFlag), + Phase2DpdAction: tunnel1Phase2DpdActionFlag.Ptr(), + Phase2StartAction: tunnel1Phase2StartActionFlag.Ptr(), + PreSharedKey: flags.FlagToStringValue(p, cmd, tunnel1PreSharedKeyFlag), + RemoteAddress: flags.FlagToStringValue(p, cmd, tunnel1RemoteAddressFlag), + }, + + Tunnel2: tunnelInputModel{ + BgpRemoteAsn: flags.FlagToInt64Pointer(p, cmd, tunnel2BgpRemoteAsnFlag), + PeeringLocalAddress: flags.FlagToStringPointer(p, cmd, tunnel2PeeringLocalAddressFlag), + PeeringRemoteAddress: flags.FlagToStringPointer(p, cmd, tunnel2PeeringRemoteAddressFlag), + Phase1DhGroups: tunnel2Phase1DhGroupsFlag.Get(), + Phase1EncryptionAlgorithms: tunnel2Phase1EncryptionAlgorithmsFlag.Get(), + Phase1IntegrityAlgorithms: tunnel2Phase1IntegrityAlgorithmsFlag.Get(), + Phase1RekeyTime: flags.FlagToInt32Pointer(p, cmd, tunnel2Phase1RekeyTimeFlag), + Phase2DhGroups: tunnel2Phase2DhGroupsFlag.Get(), + Phase2EncryptionAlgorithms: tunnel2Phase2EncryptionAlgorithmsFlag.Get(), + Phase2IntegrityAlgorithms: tunnel2Phase2IntegrityAlgorithmsFlag.Get(), + Phase2RekeyTime: flags.FlagToInt32Pointer(p, cmd, tunnel2Phase2RekeyTimeFlag), + Phase2DpdAction: tunnel2Phase2DpdActionFlag.Ptr(), + Phase2StartAction: tunnel2Phase2StartActionFlag.Ptr(), + PreSharedKey: flags.FlagToStringValue(p, cmd, tunnel2PreSharedKeyFlag), + RemoteAddress: flags.FlagToStringValue(p, cmd, tunnel2RemoteAddressFlag), + }, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildTunnelConfiguration(model *tunnelInputModel) vpn.TunnelConfiguration { + tunnel := vpn.TunnelConfiguration{ + RemoteAddress: model.RemoteAddress, + } + if model.BgpRemoteAsn != nil { + tunnel.Bgp = &vpn.BGPTunnelConfig{ + RemoteAsn: *model.BgpRemoteAsn, + } + } + if model.PeeringLocalAddress != nil || model.PeeringRemoteAddress != nil { + tunnel.Peering = &vpn.PeeringConfig{ + LocalAddress: model.PeeringLocalAddress, + RemoteAddress: model.PeeringRemoteAddress, + } + } + tunnel.Phase1 = vpn.TunnelConfigurationPhase1{ + DhGroups: model.Phase1DhGroups, + EncryptionAlgorithms: model.Phase1EncryptionAlgorithms, + IntegrityAlgorithms: model.Phase1IntegrityAlgorithms, + RekeyTime: model.Phase1RekeyTime, + } + tunnel.Phase2 = vpn.TunnelConfigurationPhase2{ + DhGroups: model.Phase2DhGroups, + EncryptionAlgorithms: model.Phase2EncryptionAlgorithms, + IntegrityAlgorithms: model.Phase2IntegrityAlgorithms, + RekeyTime: model.Phase2RekeyTime, + DpdAction: model.Phase2DpdAction, + StartAction: model.Phase2StartAction, + } + tunnel.PreSharedKey = &model.PreSharedKey + return tunnel +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) (vpn.ApiCreateGatewayConnectionRequest, error) { + req := apiClient.DefaultAPI.CreateGatewayConnection(ctx, model.ProjectId, model.Region, model.GatewayId) + + payload := vpn.CreateGatewayConnectionPayload{ + DisplayName: model.DisplayName, + Enabled: model.Enabled, + Labels: model.Labels, + LocalSubnets: model.LocalSubnets, + RemoteSubnets: model.RemoteSubnets, + StaticRoutes: model.StaticRoutes, + } + + payload.Tunnel1 = buildTunnelConfiguration(&model.Tunnel1) + payload.Tunnel2 = buildTunnelConfiguration(&model.Tunnel2) + + return req.CreateGatewayConnectionPayload(payload), nil +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *vpn.ConnectionResponse) error { + if resp == nil { + return fmt.Errorf("create response is empty") + } + return p.OutputResult(model.OutputFormat, resp, func() error { + p.Outputf("Created VPN connection %q for gateway %q in project %q.\n", utils.PtrString(resp.Id), model.GatewayId, projectLabel) + return nil + }) +} diff --git a/internal/cmd/beta/vpn/connection/create/create_test.go b/internal/cmd/beta/vpn/connection/create/create_test.go new file mode 100644 index 000000000..64d594ef1 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/create/create_test.go @@ -0,0 +1,326 @@ +package create + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/spf13/cobra" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testGatewayID = uuid.NewString() + testClient, _ = vpn.NewAPIClient( + sdkConfig.WithoutAuthentication(), + ) +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + gatewayIdFlag: testGatewayID, + displayNameFlag: "test-connection", + tunnel1RemoteAddressFlag: "1.2.3.4", + tunnel1PreSharedKeyFlag: "test-psk-1", + tunnel1Phase1EncryptionAlgorithmsFlag.Name(): "aes256", + tunnel1Phase1IntegrityAlgorithmsFlag.Name(): "sha2_256", + tunnel1Phase2EncryptionAlgorithmsFlag.Name(): "aes256", + tunnel1Phase2IntegrityAlgorithmsFlag.Name(): "sha2_256", + tunnel2RemoteAddressFlag: "5.6.7.8", + tunnel2PreSharedKeyFlag: "test-psk-2", + tunnel2Phase1EncryptionAlgorithmsFlag.Name(): "aes256", + tunnel2Phase1IntegrityAlgorithmsFlag.Name(): "sha2_256", + tunnel2Phase2EncryptionAlgorithmsFlag.Name(): "aes256", + tunnel2Phase2IntegrityAlgorithmsFlag.Name(): "sha2_256", + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + GatewayId: testGatewayID, + DisplayName: "test-connection", + Enabled: nil, + Tunnel1: tunnelInputModel{ + RemoteAddress: "1.2.3.4", + PreSharedKey: "test-psk-1", + Phase1EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + Phase1IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + Phase2EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + Phase2IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + Tunnel2: tunnelInputModel{ + RemoteAddress: "5.6.7.8", + PreSharedKey: "test-psk-2", + Phase1EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + Phase1IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + Phase2EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + Phase2IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiCreateGatewayConnectionRequest)) vpn.ApiCreateGatewayConnectionRequest { + request := testClient.DefaultAPI.CreateGatewayConnection(testCtx, testProjectId, "", testGatewayID) + payload := vpn.CreateGatewayConnectionPayload{ + DisplayName: "test-connection", + Enabled: nil, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "1.2.3.4", + PreSharedKey: utils.Ptr("test-psk-1"), + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "5.6.7.8", + PreSharedKey: utils.Ptr("test-psk-2"), + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + }, + } + request = request.CreateGatewayConnectionPayload(payload) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flags", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing project id", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "missing gateway id", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, gatewayIdFlag) + }), + isValid: false, + }, + { + description: "missing display name", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + { + description: "missing tunnel1 remote address", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, tunnel1RemoteAddressFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(printer *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(printer, cmd) + }, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult vpn.ApiCreateGatewayConnectionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + { + description: "with optional fields", + model: fixtureInputModel(func(model *inputModel) { + model.Labels = &map[string]string{"env": "prod"} + model.LocalSubnets = []string{"10.0.0.0/24"} + model.RemoteSubnets = []string{"192.168.0.0/24"} + model.StaticRoutes = []string{"10.1.0.0/24"} + model.Tunnel1.BgpRemoteAsn = utils.Ptr(int64(65000)) + model.Tunnel1.PeeringLocalAddress = utils.Ptr("169.254.0.1") + model.Tunnel1.PeeringRemoteAddress = utils.Ptr("169.254.0.2") + model.Tunnel1.Phase1DhGroups = []vpn.PhaseDhGroupsInner{"14"} + model.Tunnel1.Phase1RekeyTime = utils.Ptr(int32(3600)) + model.Tunnel1.Phase2DhGroups = []vpn.PhaseDhGroupsInner{"14"} + model.Tunnel1.Phase2RekeyTime = utils.Ptr(int32(3600)) + model.Tunnel1.Phase2DpdAction = utils.Ptr(vpn.TunnelConfigurationPhase2AllOfDpdAction("restart")) + model.Tunnel1.Phase2StartAction = utils.Ptr(vpn.TunnelConfigurationPhase2AllOfStartAction("start")) + }), + expectedResult: fixtureRequest(func(request *vpn.ApiCreateGatewayConnectionRequest) { + payload := vpn.CreateGatewayConnectionPayload{ + DisplayName: "test-connection", + Enabled: nil, + Labels: &map[string]string{"env": "prod"}, + LocalSubnets: []string{"10.0.0.0/24"}, + RemoteSubnets: []string{"192.168.0.0/24"}, + StaticRoutes: []string{"10.1.0.0/24"}, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "1.2.3.4", + PreSharedKey: utils.Ptr("test-psk-1"), + Bgp: &vpn.BGPTunnelConfig{ + RemoteAsn: 65000, + }, + Peering: &vpn.PeeringConfig{ + LocalAddress: utils.Ptr("169.254.0.1"), + RemoteAddress: utils.Ptr("169.254.0.2"), + }, + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: []vpn.PhaseDhGroupsInner{"14"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + RekeyTime: utils.Ptr(int32(3600)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: []vpn.PhaseDhGroupsInner{"14"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + RekeyTime: utils.Ptr(int32(3600)), + DpdAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfDpdAction("restart")), + StartAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfStartAction("start")), + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "5.6.7.8", + PreSharedKey: utils.Ptr("test-psk-2"), + Phase1: vpn.TunnelConfigurationPhase1{ + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + Phase2: vpn.TunnelConfigurationPhase2{ + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + }, + }, + } + *request = request.CreateGatewayConnectionPayload(payload) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult), + cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + resp *vpn.ConnectionResponse + expected string + wantErr bool + }{ + { + description: "nil response", + model: fixtureInputModel(), + resp: nil, + wantErr: true, + expected: "", + }, + { + description: "success", + model: fixtureInputModel(), + resp: &vpn.ConnectionResponse{ + Id: utils.Ptr("conn-1234"), + }, + expected: fmt.Sprintf("Created VPN connection \"conn-1234\" for gateway %q in project %q.\n", testGatewayID, testProjectId), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + err := outputResult(params.Printer, tt.model, testProjectId, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + if !tt.wantErr && params.Out.String() != tt.expected { + t.Errorf("want:\n%s\ngot:\n%s", tt.expected, params.Out.String()) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/connection/delete/delete.go b/internal/cmd/beta/vpn/connection/delete/delete.go new file mode 100644 index 000000000..1e5de918e --- /dev/null +++ b/internal/cmd/beta/vpn/connection/delete/delete.go @@ -0,0 +1,119 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +const ( + connectionIdArg = "CONNECTION_ID" + + gatewayIdFlag = "gateway-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId *string + ConnectionId string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", connectionIdArg), + Short: "Deletes a VPN connection", + Long: "Deletes a VPN connection.", + Args: args.SingleArg(connectionIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Delete a VPN connection`, + "$ stackit beta vpn connection delete xxx --gateway-id yyy"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p.Printer, p.CliVersion, cmd) + if err != nil { + p.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to delete VPN connection %q from gateway %q?", model.ConnectionId, *model.GatewayId) + err = p.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + err = req.Execute() + if err != nil { + return fmt.Errorf("delete VPN connection: %w", err) + } + + return outputResult(p.Printer, model, projectLabel) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), gatewayIdFlag, "Gateway ID") + + err := flags.MarkFlagsRequired(cmd, gatewayIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + connectionId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: flags.FlagToStringPointer(p, cmd, gatewayIdFlag), + ConnectionId: connectionId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) (vpn.ApiDeleteGatewayConnectionRequest, error) { + req := apiClient.DefaultAPI.DeleteGatewayConnection(ctx, model.ProjectId, model.Region, *model.GatewayId, model.ConnectionId) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string) error { + p.Outputf("deleted VPN connection %q for gateway %q in project %q.\n", model.ConnectionId, *model.GatewayId, projectLabel) + return nil +} diff --git a/internal/cmd/beta/vpn/connection/delete/delete_test.go b/internal/cmd/beta/vpn/connection/delete/delete_test.go new file mode 100644 index 000000000..1d2eee692 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/delete/delete_test.go @@ -0,0 +1,156 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testGatewayID = uuid.NewString() + testConnectionID = uuid.NewString() + testClient, _ = vpn.NewAPIClient( + sdkConfig.WithoutAuthentication(), + ) +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testConnectionID, + } + for _, m := range mods { + m(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + gatewayIdFlag: testGatewayID, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + GatewayId: &testGatewayID, + ConnectionId: testConnectionID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiDeleteGatewayConnectionRequest)) vpn.ApiDeleteGatewayConnectionRequest { + request := testClient.DefaultAPI.DeleteGatewayConnection(testCtx, testProjectId, "", testGatewayID, testConnectionID) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no gateway id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, gatewayIdFlag) + }), + isValid: false, + }, + { + description: "no project id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult vpn.ApiDeleteGatewayConnectionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult), + cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/connection/describe/describe.go b/internal/cmd/beta/vpn/connection/describe/describe.go new file mode 100644 index 000000000..16f581821 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/describe/describe.go @@ -0,0 +1,170 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + connectionIdArg = "CONNECTION_ID" + + gatewayIdFlag = "gateway-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId *string + ConnectionId string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", connectionIdArg), + Short: "Shows details of a VPN connection", + Long: "Shows details of a VPN connection.", + Args: args.SingleArg(connectionIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Show details of a VPN connection`, + "$ stackit beta vpn connection describe xxx --gateway-id yyy"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe VPN connection: %w", err) + } + + return outputResult(p.Printer, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), gatewayIdFlag, "Gateway ID") + + err := flags.MarkFlagsRequired(cmd, gatewayIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + connectionId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: flags.FlagToStringPointer(p, cmd, gatewayIdFlag), + ConnectionId: connectionId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) (vpn.ApiGetGatewayConnectionRequest, error) { + req := apiClient.DefaultAPI.GetGatewayConnection(ctx, model.ProjectId, model.Region, *model.GatewayId, model.ConnectionId) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *vpn.ConnectionResponse) error { + if resp == nil { + return fmt.Errorf("describe response is empty") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + mainTable := tables.NewTable() + mainTable.AddRow("ID", utils.PtrString(resp.Id)) + mainTable.AddRow("Name", resp.DisplayName) + mainTable.AddRow("Enabled", utils.PtrString(resp.Enabled)) + var labels string + if resp.Labels != nil { + labels = utils.JoinStringMap(*resp.Labels, "=", ", ") + } + mainTable.AddRow("Labels", labels) + mainTable.AddRow("Local Subnets", strings.Join(resp.LocalSubnets, ", ")) + mainTable.AddRow("Remote Subnets", strings.Join(resp.RemoteSubnets, ", ")) + mainTable.AddRow("Static Routes", strings.Join(resp.StaticRoutes, ", ")) + + ts := []tables.Table{ + mainTable, + } + ts = append(ts, tunnelTables(&resp.Tunnel1, "Tunnel 1")...) + ts = append(ts, tunnelTables(&resp.Tunnel2, "Tunnel 2")...) + return tables.DisplayTables(p, ts) + }) +} + +func tunnelTables(tunnel *vpn.TunnelConfiguration, title string) []tables.Table { + table := tables.NewTable() + table.SetTitle(title) + table.AddRow("IP Address", tunnel.RemoteAddress) + var bgp string + if tunnel.Bgp != nil { + bgp = fmt.Sprintf("%d", tunnel.Bgp.RemoteAsn) + } + table.AddRow("BGP ASN", bgp) + var peering string + if tunnel.Peering != nil { + peering = fmt.Sprintf("%s/%s", utils.PtrString(tunnel.Peering.LocalAddress), utils.PtrString(tunnel.Peering.RemoteAddress)) + } + table.AddRow("Peering (local/remote)", peering) + + phase1Table := tables.NewTable() + phase1Table.SetTitle(fmt.Sprintf("%s Phase 1", title)) + phase1Table.AddRow("DH Groups", utils.JoinStringPtr(&tunnel.Phase1.DhGroups, ", ")) + phase1Table.AddRow("Encryption Algos", utils.JoinStringPtr(&tunnel.Phase1.EncryptionAlgorithms, ", ")) + phase1Table.AddRow("Integrity Algos", utils.JoinStringPtr(&tunnel.Phase1.IntegrityAlgorithms, ", ")) + phase1Table.AddRow("Rekey Time", utils.PtrString(tunnel.Phase1.RekeyTime)) + + phase2Table := tables.NewTable() + phase2Table.SetTitle(fmt.Sprintf("%s Phase 2", title)) + phase2Table.AddRow("DH Groups", utils.JoinStringPtr(&tunnel.Phase2.DhGroups, ", ")) + phase2Table.AddRow("Encryption Algos", utils.JoinStringPtr(&tunnel.Phase2.EncryptionAlgorithms, ", ")) + phase2Table.AddRow("Integrity Algos", utils.JoinStringPtr(&tunnel.Phase2.IntegrityAlgorithms, ", ")) + phase2Table.AddRow("Rekey Time", utils.PtrString(tunnel.Phase1.RekeyTime)) + phase2Table.AddRow("Dpd Action", utils.PtrString(tunnel.Phase2.DpdAction)) + phase2Table.AddRow("Start Action", utils.PtrString(tunnel.Phase2.StartAction)) + + return []tables.Table{ + table, + phase1Table, + phase2Table, + } +} diff --git a/internal/cmd/beta/vpn/connection/describe/describe_test.go b/internal/cmd/beta/vpn/connection/describe/describe_test.go new file mode 100644 index 000000000..c9f42ef62 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/describe/describe_test.go @@ -0,0 +1,243 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testGatewayID = uuid.NewString() + testConnectionID = uuid.NewString() + testClient, _ = vpn.NewAPIClient( + sdkConfig.WithoutAuthentication(), + ) +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testConnectionID, + } + for _, m := range mods { + m(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + gatewayIdFlag: testGatewayID, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + GatewayId: utils.Ptr(testGatewayID), + ConnectionId: testConnectionID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiGetGatewayConnectionRequest)) vpn.ApiGetGatewayConnectionRequest { + request := testClient.DefaultAPI.GetGatewayConnection(testCtx, testProjectId, "", testGatewayID, testConnectionID) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureResponse(mods ...func(resp *vpn.ConnectionResponse)) *vpn.ConnectionResponse { + resp := &vpn.ConnectionResponse{ + Id: utils.Ptr(testConnectionID), + DisplayName: "test-connection", + Enabled: utils.Ptr(true), + Labels: &map[string]string{ + "env": "prod", + }, + LocalSubnets: []string{"10.0.0.0/24"}, + RemoteSubnets: []string{"192.168.0.0/24"}, + StaticRoutes: []string{"10.1.0.0/24"}, + Tunnel1: vpn.TunnelConfiguration{ + RemoteAddress: "1.2.3.4", + Bgp: &vpn.BGPTunnelConfig{ + RemoteAsn: 65000, + }, + Peering: &vpn.PeeringConfig{ + LocalAddress: utils.Ptr("169.254.0.1"), + RemoteAddress: utils.Ptr("169.254.0.2"), + }, + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: []vpn.PhaseDhGroupsInner{"14"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + RekeyTime: utils.Ptr(int32(3600)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: []vpn.PhaseDhGroupsInner{"14"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + RekeyTime: utils.Ptr(int32(3600)), + DpdAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfDpdAction("restart")), + StartAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfStartAction("start")), + }, + }, + Tunnel2: vpn.TunnelConfiguration{ + RemoteAddress: "5.6.7.8", + Phase1: vpn.TunnelConfigurationPhase1{ + DhGroups: []vpn.PhaseDhGroupsInner{"14"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + RekeyTime: utils.Ptr(int32(3600)), + }, + Phase2: vpn.TunnelConfigurationPhase2{ + DhGroups: []vpn.PhaseDhGroupsInner{"14"}, + EncryptionAlgorithms: []vpn.PhaseEncryptionAlgorithmsInner{"aes256"}, + IntegrityAlgorithms: []vpn.PhaseIntegrityAlgorithmsInner{"sha2_256"}, + RekeyTime: utils.Ptr(int32(3600)), + DpdAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfDpdAction("restart")), + StartAction: utils.Ptr(vpn.TunnelConfigurationPhase2AllOfStartAction("start")), + }, + }, + } + + for _, mod := range mods { + mod(resp) + } + return resp +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no args", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no gateway id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, gatewayIdFlag) + }), + isValid: false, + }, + { + description: "no project id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult vpn.ApiGetGatewayConnectionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult), + cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + resp *vpn.ConnectionResponse + wantErr bool + }{ + { + description: "nil response", + model: fixtureInputModel(), + resp: nil, + wantErr: true, + }, + { + description: "full response", + model: fixtureInputModel(), + resp: fixtureResponse(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + err := outputResult(params.Printer, tt.model, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/connection/list/list.go b/internal/cmd/beta/vpn/connection/list/list.go new file mode 100644 index 000000000..02bb5356d --- /dev/null +++ b/internal/cmd/beta/vpn/connection/list/list.go @@ -0,0 +1,120 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + gatewayIdFlag = "gateway-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId *string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all VPN connections of a gateway", + Long: "Lists all VPN connections of a gateway.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all VPN connections of a gateway`, + "$ stackit beta vpn connection list --gateway-id xxx"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list VPN connections: %w", err) + } + + return outputResult(p.Printer, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), gatewayIdFlag, "Gateway ID") + + err := flags.MarkFlagsRequired(cmd, gatewayIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: flags.FlagToStringPointer(p, cmd, gatewayIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) (vpn.ApiListGatewayConnectionsRequest, error) { + req := apiClient.DefaultAPI.ListGatewayConnections(ctx, model.ProjectId, model.Region, *model.GatewayId) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *vpn.ConnectionList) error { + if resp == nil || resp.Connections == nil { + return fmt.Errorf("list connections response is empty") + } + + return p.OutputResult(model.OutputFormat, resp.Connections, func() error { + table := tables.NewTable() + table.SetHeader("ID", "NAME", "ENABLED", "LABELS") + for _, c := range resp.Connections { + id := utils.PtrString(c.Id) + name := c.DisplayName + enabled := utils.PtrString(c.Enabled) + var labels string + if c.Labels != nil { + labels = utils.JoinStringMap(*c.Labels, "=", ", ") + } + table.AddRow(id, name, enabled, labels) + } + p.Outputln(table.Render()) + return nil + }) +} diff --git a/internal/cmd/beta/vpn/connection/list/list_test.go b/internal/cmd/beta/vpn/connection/list/list_test.go new file mode 100644 index 000000000..4ddf3db73 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/list/list_test.go @@ -0,0 +1,207 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/spf13/cobra" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testGatewayID = uuid.NewString() + testClient, _ = vpn.NewAPIClient( + sdkConfig.WithoutAuthentication(), + ) +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + gatewayIdFlag: testGatewayID, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + GatewayId: utils.Ptr(testGatewayID), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiListGatewayConnectionsRequest)) vpn.ApiListGatewayConnectionsRequest { + request := testClient.DefaultAPI.ListGatewayConnections(testCtx, testProjectId, "", testGatewayID) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flags", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no project id", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "no gateway id", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, gatewayIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, func(printer *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + return parseInput(printer, cmd) + }, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedResult vpn.ApiListGatewayConnectionsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedResult: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + diff := cmp.Diff(request, tt.expectedResult, + cmp.AllowUnexported(tt.expectedResult), + cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + resp *vpn.ConnectionList + wantErr bool + }{ + { + description: "empty list", + model: fixtureInputModel(), + resp: &vpn.ConnectionList{ + Connections: []vpn.ConnectionResponse{}, + }, + }, + { + description: "nil response", + model: fixtureInputModel(), + resp: nil, + wantErr: true, + }, + { + description: "nil connections", + model: fixtureInputModel(), + resp: &vpn.ConnectionList{ + Connections: nil, + }, + wantErr: true, + }, + { + description: "with entries", + model: fixtureInputModel(), + resp: &vpn.ConnectionList{ + Connections: []vpn.ConnectionResponse{ + { + Id: utils.Ptr("conn-1"), + DisplayName: "test-conn-1", + Enabled: utils.Ptr(true), + Labels: &map[string]string{ + "env": "prod", + }, + }, + { + Id: utils.Ptr("conn-2"), + DisplayName: "test-conn-2", + Enabled: utils.Ptr(false), + Labels: nil, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + err := outputResult(params.Printer, tt.model, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/connection/status/status.go b/internal/cmd/beta/vpn/connection/status/status.go new file mode 100644 index 000000000..6f3f91c46 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/status/status.go @@ -0,0 +1,164 @@ +package status + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/vpn/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + connectionIdArg = "CONNECTION_ID" + + gatewayIdFlag = "gateway-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + GatewayId *string + ConnectionId string +} + +func NewCmd(p *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("status %s", connectionIdArg), + Short: "Shows the status of a VPN connection", + Long: "Shows the status of a VPN connection.", + Args: args.SingleArg(connectionIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Show status of a VPN connection`, + "$ stackit beta vpn connection status xxx --gateway-id yyy"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p.Printer, p.CliVersion) + if err != nil { + return err + } + + // Call API + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get VPN connection status: %w", err) + } + + return outputResult(p.Printer, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), gatewayIdFlag, "Gateway ID") + + err := flags.MarkFlagsRequired(cmd, gatewayIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + connectionId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + GatewayId: flags.FlagToStringPointer(p, cmd, gatewayIdFlag), + ConnectionId: connectionId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *vpn.APIClient) (vpn.ApiGetGatewayConnectionStatusRequest, error) { + req := apiClient.DefaultAPI.GetGatewayConnectionStatus(ctx, model.ProjectId, model.Region, *model.GatewayId, model.ConnectionId) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *vpn.ConnectionStatusResponse) error { + if resp == nil { + return fmt.Errorf("status response is empty") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + mainTable := tables.NewTable() + mainTable.AddRow("ID", utils.PtrString(resp.Id)) + mainTable.AddRow("Name", utils.PtrString(resp.DisplayName)) + mainTable.AddRow("Enabled", utils.PtrString(resp.Enabled)) + + ts := []tables.Table{ + mainTable, + } + for _, tunnel := range resp.Tunnels { + ts = append(ts, tunnelTables(&tunnel)...) + } + + return tables.DisplayTables(p, ts) + }) +} + +func tunnelTables(tunnel *vpn.TunnelStatus) []tables.Table { + title := "Tunnel" + if tunnel.Name != nil { + title = string(*tunnel.Name) + } + + table := tables.NewTable() + table.SetTitle(title) + table.AddRow("Established", utils.PtrString(tunnel.Established)) + + res := []tables.Table{table} + + if tunnel.Phase1 != nil { + phase1Table := tables.NewTable() + phase1Table.SetTitle(fmt.Sprintf("%s Phase 1", title)) + phase1Table.AddRow("State", utils.PtrString(tunnel.Phase1.State)) + phase1Table.AddRow("DH Group", utils.PtrString(tunnel.Phase1.DhGroup)) + phase1Table.AddRow("Encryption Algo", utils.PtrString(tunnel.Phase1.EncryptionAlgorithm)) + phase1Table.AddRow("Integrity Algo", utils.PtrString(tunnel.Phase1.IntegrityAlgorithm)) + res = append(res, phase1Table) + } + + if tunnel.Phase2 != nil { + phase2Table := tables.NewTable() + phase2Table.SetTitle(fmt.Sprintf("%s Phase 2", title)) + phase2Table.AddRow("State", utils.PtrString(tunnel.Phase2.State)) + phase2Table.AddRow("Protocol", utils.PtrString(tunnel.Phase2.Protocol)) + phase2Table.AddRow("DH Group", utils.PtrString(tunnel.Phase2.DhGroup)) + phase2Table.AddRow("Encryption Algo", utils.PtrString(tunnel.Phase2.EncryptionAlgorithm)) + phase2Table.AddRow("Integrity Algo", utils.PtrString(tunnel.Phase2.IntegrityAlgorithm)) + phase2Table.AddRow("Encap", utils.PtrString(tunnel.Phase2.Encap)) + phase2Table.AddRow("Bytes In/Out", fmt.Sprintf("%s / %s", utils.PtrString(tunnel.Phase2.BytesIn), utils.PtrString(tunnel.Phase2.BytesOut))) + phase2Table.AddRow("Packets In/Out", fmt.Sprintf("%s / %s", utils.PtrString(tunnel.Phase2.PacketsIn), utils.PtrString(tunnel.Phase2.PacketsOut))) + res = append(res, phase2Table) + } + + return res +} diff --git a/internal/cmd/beta/vpn/connection/status/status_test.go b/internal/cmd/beta/vpn/connection/status/status_test.go new file mode 100644 index 000000000..b43ab13a6 --- /dev/null +++ b/internal/cmd/beta/vpn/connection/status/status_test.go @@ -0,0 +1,219 @@ +package status + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") + testProjectId = uuid.NewString() + testGatewayID = uuid.NewString() + testConnectionID = uuid.NewString() + testClient, _ = vpn.NewAPIClient( + sdkConfig.WithoutAuthentication(), + ) +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testConnectionID, + } + for _, m := range mods { + m(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + gatewayIdFlag: testGatewayID, + } + for _, m := range mods { + m(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + }, + GatewayId: utils.Ptr(testGatewayID), + ConnectionId: testConnectionID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *vpn.ApiGetGatewayConnectionStatusRequest)) vpn.ApiGetGatewayConnectionStatusRequest { + request := testClient.DefaultAPI.GetGatewayConnectionStatus(testCtx, testProjectId, "", testGatewayID, testConnectionID) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureResponse(mods ...func(resp *vpn.ConnectionStatusResponse)) *vpn.ConnectionStatusResponse { + resp := &vpn.ConnectionStatusResponse{ + Id: utils.Ptr(testConnectionID), + DisplayName: utils.Ptr("test-connection"), + Enabled: utils.Ptr(true), + Tunnels: []vpn.TunnelStatus{ + { + Name: utils.Ptr(vpn.TunnelStatusName("tunnel1")), + Established: utils.Ptr(true), + Phase1: &vpn.Phase1Status{ + DhGroup: utils.Ptr("MODP2048"), + EncryptionAlgorithm: utils.Ptr("AES_GCM_16"), + IntegrityAlgorithm: utils.Ptr("SHA_256"), + State: utils.Ptr("INSTALLED"), + }, + Phase2: &vpn.Phase2Status{ + BytesIn: utils.Ptr("453533"), + BytesOut: utils.Ptr("46459064"), + DhGroup: utils.Ptr("MODP2048"), + Encap: utils.Ptr("yes"), + EncryptionAlgorithm: utils.Ptr("AES_GCM_16"), + IntegrityAlgorithm: utils.Ptr("SHA_256"), + PacketsIn: utils.Ptr("1534134"), + PacketsOut: utils.Ptr("65847343"), + Protocol: utils.Ptr("ESP"), + State: utils.Ptr("ESTABLISHED"), + }, + }, + }, + } + for _, mod := range mods { + mod(resp) + } + return resp +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no args", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no gateway id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, gatewayIdFlag) + }), + isValid: false, + }, + { + description: "no project id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + name string + model *inputModel + expected vpn.ApiGetGatewayConnectionStatusRequest + }{ + { + name: "base", + model: fixtureInputModel(), + expected: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + diff := cmp.Diff(request, tt.expected, + cmp.AllowUnexported(tt.expected), + cmpopts.IgnoreUnexported(vpn.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + resp *vpn.ConnectionStatusResponse + wantErr bool + }{ + { + description: "nil response", + model: fixtureInputModel(), + resp: nil, + wantErr: true, + }, + { + description: "full response", + model: fixtureInputModel(), + resp: fixtureResponse(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + err := outputResult(params.Printer, tt.model, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/beta/vpn/vpn.go b/internal/cmd/beta/vpn/vpn.go new file mode 100644 index 000000000..8ee353974 --- /dev/null +++ b/internal/cmd/beta/vpn/vpn.go @@ -0,0 +1,25 @@ +package vpn + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/beta/vpn/connection" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "vpn", + Short: "Provides functionality for VPN", + Long: "Provides functionality for VPN.", + Args: cobra.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(connection.NewCmd(params)) +} diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 46b21b96a..3000a8ac1 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -3,18 +3,19 @@ package config import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/list" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile" "github.com/stackitcloud/stackit-cli/internal/cmd/config/set" "github.com/stackitcloud/stackit-cli/internal/cmd/config/unset" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Provides functionality for CLI configuration options", @@ -32,7 +33,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(set.NewCmd(params)) cmd.AddCommand(unset.NewCmd(params)) diff --git a/internal/cmd/config/list/list.go b/internal/cmd/config/list/list.go index 6dbc78e7e..99112637a 100644 --- a/internal/cmd/config/list/list.go +++ b/internal/cmd/config/list/list.go @@ -4,13 +4,15 @@ import ( "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "slices" "sort" "strconv" "strings" "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -26,7 +28,7 @@ type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists the current CLI configuration values", diff --git a/internal/cmd/config/list/list_test.go b/internal/cmd/config/list/list_test.go index cbdab8410..8ec98fff3 100644 --- a/internal/cmd/config/list/list_test.go +++ b/internal/cmd/config/list/list_test.go @@ -3,9 +3,7 @@ package list import ( "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) func TestOutputResult(t *testing.T) { @@ -25,11 +23,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.configData, tt.args.activeProfile); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.configData, tt.args.activeProfile); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/config/profile/create/create.go b/internal/cmd/config/profile/create/create.go index 4d93bdd72..e87ed5838 100644 --- a/internal/cmd/config/profile/create/create.go +++ b/internal/cmd/config/profile/create/create.go @@ -3,7 +3,7 @@ package create import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" @@ -19,27 +19,30 @@ import ( const ( profileArg = "PROFILE" - noSetFlag = "no-set" - fromEmptyProfile = "empty" + noSetFlag = "no-set" + ignoreExistingFlag = "ignore-existing" + fromEmptyProfile = "empty" ) type inputModel struct { *globalflags.GlobalFlagModel NoSet bool + IgnoreExisting bool FromEmptyProfile bool Profile string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("create %s", profileArg), Short: "Creates a CLI configuration profile", - Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", + Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", "Creates a CLI configuration profile based on the currently active profile and sets it as active.", `The profile name can be provided via the STACKIT_CLI_PROFILE environment variable or as an argument in this command.`, "The environment variable takes precedence over the argument.", "If you do not want to set the profile as active, use the --no-set flag.", "If you want to create the new profile with the initial default configurations, use the --empty flag.", + "If you want to create the new profile and ignore the error for an already existing profile, use the --ignore-existing flag.", ), Args: args.SingleArg(profileArg, nil), Example: examples.Build( @@ -56,7 +59,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - err = config.CreateProfile(params.Printer, model.Profile, !model.NoSet, model.FromEmptyProfile) + err = config.CreateProfile(params.Printer, model.Profile, !model.NoSet, model.IgnoreExisting, model.FromEmptyProfile) if err != nil { return fmt.Errorf("create profile: %w", err) } @@ -85,6 +88,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(noSetFlag, false, "Do not set the profile as the active profile") + cmd.Flags().Bool(ignoreExistingFlag, false, "Suppress the error if the profile exists already. An existing profile will not be modified or overwritten") cmd.Flags().Bool(fromEmptyProfile, false, "Create the profile with the initial default configurations") } @@ -103,16 +107,9 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Profile: profile, FromEmptyProfile: flags.FlagToBoolValue(p, cmd, fromEmptyProfile), NoSet: flags.FlagToBoolValue(p, cmd, noSetFlag), + IgnoreExisting: flags.FlagToBoolValue(p, cmd, ignoreExistingFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/config/profile/create/create_test.go b/internal/cmd/config/profile/create/create_test.go index 6bf77a7ae..8d214e8d4 100644 --- a/internal/cmd/config/profile/create/create_test.go +++ b/internal/cmd/config/profile/create/create_test.go @@ -3,12 +3,8 @@ package create import ( "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - - "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const testProfile = "test-profile" @@ -67,11 +63,11 @@ func TestParseInput(t *testing.T) { description: "some global flag", argValues: fixtureArgValues(), flagValues: map[string]string{ - globalflags.VerbosityFlag: globalflags.DebugVerbosity, + globalflags.VerbosityFlag.Name(): globalflags.DebugVerbosity, }, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity + model.Verbosity = globalflags.DebugVerbosity }), }, { @@ -105,54 +101,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/config/profile/delete/delete.go b/internal/cmd/config/profile/delete/delete.go index 02c04e64a..d94f71479 100644 --- a/internal/cmd/config/profile/delete/delete.go +++ b/internal/cmd/config/profile/delete/delete.go @@ -3,7 +3,7 @@ package delete import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" @@ -25,7 +25,7 @@ type inputModel struct { Profile string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", profileArg), Short: "Delete a CLI configuration profile", @@ -65,17 +65,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { params.Printer.Warn("The profile you are trying to delete is the active profile. The default profile will be set to active.\n") } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } - } - - err = config.DeleteProfile(params.Printer, model.Profile) + prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) + err = params.Printer.PromptForConfirmation(prompt) if err != nil { - return fmt.Errorf("delete profile: %w", err) + return err } err = auth.DeleteProfileAuth(model.Profile) @@ -83,6 +76,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("delete profile authentication: %w", err) } + err = config.DeleteProfile(params.Printer, model.Profile) + if err != nil { + return fmt.Errorf("delete profile: %w", err) + } + params.Printer.Info("Successfully deleted profile %q\n", model.Profile) return nil @@ -106,14 +104,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Profile: profile, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/config/profile/delete/delete_test.go b/internal/cmd/config/profile/delete/delete_test.go index 66374b69d..cb98a23c0 100644 --- a/internal/cmd/config/profile/delete/delete_test.go +++ b/internal/cmd/config/profile/delete/delete_test.go @@ -1,14 +1,17 @@ package delete import ( + "fmt" + "os" "testing" + "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/zalando/go-keyring" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - - "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const testProfile = "test-profile" @@ -65,11 +68,11 @@ func TestParseInput(t *testing.T) { description: "some global flag", argValues: fixtureArgValues(), flagValues: map[string]string{ - globalflags.VerbosityFlag: globalflags.DebugVerbosity, + globalflags.VerbosityFlag.Name(): globalflags.DebugVerbosity, }, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity + model.Verbosity = globalflags.DebugVerbosity }), }, { @@ -81,54 +84,37 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } + +func TestDeleteProfileWithoutKeyring(t *testing.T) { + params := testparams.NewTestParams() + params.Printer.AssumeYes = true + profile := fmt.Sprintf("test-profile-%s", time.Now().Format("20060102150405")) + path := config.GetProfileFolderPath(profile) + t.Cleanup(func() { + err := os.RemoveAll(path) + if err != nil { + t.Fatalf("cleanup: remove profile folder at path %q: %v", path, err) + } + }) + err := config.ValidateProfile(profile) + if err != nil { + t.Fatalf("validate profile %q: %v", profile, err) + } + err = config.CreateProfile(params.Printer, profile, true, false, true) + if err != nil { + t.Fatalf("create profile %q: %v", profile, err) + } + keyring.MockInitWithError(keyring.ErrUnsupportedPlatform) + deleteCmd := NewCmd(params.CmdParams) + err = deleteCmd.RunE(deleteCmd, []string{profile}) + if err != nil { + t.Fatalf("run cmd: %v", err) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("expected profile folder to be deleted, but it still exists at path %q", path) + } +} diff --git a/internal/cmd/config/profile/export/export.go b/internal/cmd/config/profile/export/export.go index 59389b47a..631ca975c 100644 --- a/internal/cmd/config/profile/export/export.go +++ b/internal/cmd/config/profile/export/export.go @@ -4,7 +4,8 @@ import ( "fmt" "path/filepath" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -29,7 +30,7 @@ type inputModel struct { FilePath string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("export %s", profileNameArg), Short: "Exports a CLI configuration profile", @@ -85,14 +86,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model.FilePath = filepath.Join(model.FilePath, exportFileName) } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/config/profile/export/export_test.go b/internal/cmd/config/profile/export/export_test.go index 5975cf783..dc67621da 100644 --- a/internal/cmd/config/profile/export/export_test.go +++ b/internal/cmd/config/profile/export/export_test.go @@ -4,12 +4,8 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - - "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const ( @@ -103,54 +99,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/config/profile/import/import.go b/internal/cmd/config/profile/import/import.go index 84bd9b3aa..3761ec3fa 100644 --- a/internal/cmd/config/profile/import/import.go +++ b/internal/cmd/config/profile/import/import.go @@ -2,7 +2,7 @@ package importProfile import ( "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -10,6 +10,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" ) const ( @@ -25,7 +26,7 @@ type inputModel struct { NoSet bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "import", Short: "Imports a CLI configuration profile", @@ -41,8 +42,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { ), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { - model, err := parseInput(params.Printer, cmd) + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,7 +71,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(cmd.MarkFlagRequired(configFlag)) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := &inputModel{ @@ -94,14 +95,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return model, nil } diff --git a/internal/cmd/config/profile/import/import_test.go b/internal/cmd/config/profile/import/import_test.go index 121b7adb9..e676f1b14 100644 --- a/internal/cmd/config/profile/import/import_test.go +++ b/internal/cmd/config/profile/import/import_test.go @@ -5,11 +5,8 @@ import ( "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - - "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const testProfile = "test-profile" @@ -49,6 +46,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -75,45 +73,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - t.Fatalf("error parsing input: %v", err) - } - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(tt.expectedModel, model) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/config/profile/import/template/profile.json b/internal/cmd/config/profile/import/template/profile.json index ab56ce66b..ed2702e7e 100644 --- a/internal/cmd/config/profile/import/template/profile.json +++ b/internal/cmd/config/profile/import/template/profile.json @@ -3,6 +3,7 @@ "async": false, "authorization_custom_endpoint": "", "dns_custom_endpoint": "", + "edge_custom_endpoint": "", "iaas_custom_endpoint": "", "identity_provider_custom_client_id": "", "identity_provider_custom_well_known_configuration": "", @@ -25,9 +26,10 @@ "serverbackup_custom_endpoint": "", "service_account_custom_endpoint": "", "service_enablement_custom_endpoint": "", - "session_time_limit": "2h", + "session_time_limit": "12h", + "sfs_custom_endpoint": "", "ske_custom_endpoint": "", "sqlserverflex_custom_endpoint": "", "token_custom_endpoint": "", "verbosity": "info" -} \ No newline at end of file +} diff --git a/internal/cmd/config/profile/list/list.go b/internal/cmd/config/profile/list/list.go index 86e2f3341..1707225c1 100644 --- a/internal/cmd/config/profile/list/list.go +++ b/internal/cmd/config/profile/list/list.go @@ -1,12 +1,10 @@ package list import ( - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" @@ -22,7 +20,7 @@ type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all CLI configuration profiles", @@ -93,22 +91,7 @@ func buildOutput(profiles []string, activeProfile string) []profileInfo { } func outputResult(p *print.Printer, outputFormat string, profiles []profileInfo) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(profiles, "", " ") - if err != nil { - return fmt.Errorf("marshal config list: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(profiles, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal config list: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, profiles, func() error { table := tables.NewTable() table.SetHeader("NAME", "ACTIVE", "EMAIL") for _, profile := range profiles { @@ -129,5 +112,5 @@ func outputResult(p *print.Printer, outputFormat string, profiles []profileInfo) return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/config/profile/list/list_test.go b/internal/cmd/config/profile/list/list_test.go index 8016b6071..df30621b9 100644 --- a/internal/cmd/config/profile/list/list_test.go +++ b/internal/cmd/config/profile/list/list_test.go @@ -3,8 +3,7 @@ package list import ( "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) func TestOutputResult(t *testing.T) { @@ -23,11 +22,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.profiles); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.profiles); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/config/profile/profile.go b/internal/cmd/config/profile/profile.go index f6ad03ece..ab13f07cc 100644 --- a/internal/cmd/config/profile/profile.go +++ b/internal/cmd/config/profile/profile.go @@ -3,6 +3,8 @@ package profile import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/create" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/export" @@ -10,14 +12,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/list" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/unset" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "profile", Short: "Manage the CLI configuration profiles", @@ -34,7 +35,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(set.NewCmd(params)) cmd.AddCommand(unset.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) diff --git a/internal/cmd/config/profile/set/set.go b/internal/cmd/config/profile/set/set.go index 0784d4654..d5436a561 100644 --- a/internal/cmd/config/profile/set/set.go +++ b/internal/cmd/config/profile/set/set.go @@ -3,7 +3,8 @@ package set import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" @@ -24,7 +25,7 @@ type inputModel struct { Profile string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("set %s", profileArg), Short: "Set a CLI configuration profile", @@ -90,14 +91,6 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Profile: profile, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/config/profile/set/set_test.go b/internal/cmd/config/profile/set/set_test.go index 67b5f0789..e718ab0eb 100644 --- a/internal/cmd/config/profile/set/set_test.go +++ b/internal/cmd/config/profile/set/set_test.go @@ -3,12 +3,8 @@ package set import ( "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - - "github.com/google/go-cmp/cmp" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const testProfile = "test-profile" @@ -65,11 +61,11 @@ func TestParseInput(t *testing.T) { description: "some global flag", argValues: fixtureArgValues(), flagValues: map[string]string{ - globalflags.VerbosityFlag: globalflags.DebugVerbosity, + globalflags.VerbosityFlag.Name(): globalflags.DebugVerbosity, }, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity + model.Verbosity = globalflags.DebugVerbosity }), }, { @@ -81,54 +77,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/config/profile/unset/unset.go b/internal/cmd/config/profile/unset/unset.go index 9c7f923cd..d56fcfa14 100644 --- a/internal/cmd/config/profile/unset/unset.go +++ b/internal/cmd/config/profile/unset/unset.go @@ -3,7 +3,8 @@ package unset import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" @@ -13,7 +14,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "unset", Short: "Unset the current active CLI configuration profile", diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 54f9f527d..9bb2713b5 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -4,7 +4,8 @@ import ( "fmt" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -25,6 +26,7 @@ const ( authorizationCustomEndpointFlag = "authorization-custom-endpoint" dnsCustomEndpointFlag = "dns-custom-endpoint" + edgeCustomEndpointFlag = "edge-custom-endpoint" loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint" logMeCustomEndpointFlag = "logme-custom-endpoint" mariaDBCustomEndpointFlag = "mariadb-custom-endpoint" @@ -37,6 +39,7 @@ const ( redisCustomEndpointFlag = "redis-custom-endpoint" resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint" + kmsCustomEndpointFlag = "kms-custom-endpoint" serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint" runCommandCustomEndpointFlag = "runcommand-custom-endpoint" @@ -46,6 +49,10 @@ const ( sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" iaasCustomEndpointFlag = "iaas-custom-endpoint" tokenCustomEndpointFlag = "token-custom-endpoint" + intakeCustomEndpointFlag = "intake-custom-endpoint" + logsCustomEndpointFlag = "logs-custom-endpoint" + sfsCustomEndpointFlag = "sfs-custom-endpoint" + cdnCustomEndpointFlag = "cdn-custom-endpoint" ) type inputModel struct { @@ -54,7 +61,7 @@ type inputModel struct { ProjectIdSet bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "set", Short: "Sets CLI configuration options", @@ -76,8 +83,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Set the DNS custom endpoint. This endpoint will be used on all calls to the DNS API (unless overridden by the "STACKIT_DNS_CUSTOM_ENDPOINT" environment variable)`, "$ stackit config set --dns-custom-endpoint https://dns.stackit.cloud"), ), - RunE: func(cmd *cobra.Command, _ []string) error { - model, err := parseInput(params.Printer, cmd) + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -132,13 +139,14 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.{{e } func configureFlags(cmd *cobra.Command) { - cmd.Flags().String(sessionTimeLimitFlag, "", "Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect)") + cmd.Flags().String(sessionTimeLimitFlag, "", "Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s") cmd.Flags().String(identityProviderCustomWellKnownConfigurationFlag, "", "Identity Provider well-known OpenID configuration URL, used for user authentication") cmd.Flags().String(identityProviderCustomClientIdFlag, "", "Identity Provider client ID, used for user authentication") cmd.Flags().String(allowedUrlDomainFlag, "", `Domain name, used for the verification of the URLs that are given in the custom identity provider endpoint and "STACKIT curl" command`) cmd.Flags().String(observabilityCustomEndpointFlag, "", "Observability API base URL, used in calls to this API") cmd.Flags().String(authorizationCustomEndpointFlag, "", "Authorization API base URL, used in calls to this API") cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API") + cmd.Flags().String(edgeCustomEndpointFlag, "", "Edge API base URL, used in calls to this API") cmd.Flags().String(loadBalancerCustomEndpointFlag, "", "Load Balancer API base URL, used in calls to this API") cmd.Flags().String(logMeCustomEndpointFlag, "", "LogMe API base URL, used in calls to this API") cmd.Flags().String(mariaDBCustomEndpointFlag, "", "MariaDB API base URL, used in calls to this API") @@ -150,6 +158,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(redisCustomEndpointFlag, "", "Redis API base URL, used in calls to this API") cmd.Flags().String(resourceManagerCustomEndpointFlag, "", "Resource Manager API base URL, used in calls to this API") cmd.Flags().String(secretsManagerCustomEndpointFlag, "", "Secrets Manager API base URL, used in calls to this API") + cmd.Flags().String(kmsCustomEndpointFlag, "", "KMS API base URL, used in calls to this API") cmd.Flags().String(serviceAccountCustomEndpointFlag, "", "Service Account API base URL, used in calls to this API") cmd.Flags().String(serviceEnablementCustomEndpointFlag, "", "Service Enablement API base URL, used in calls to this API") cmd.Flags().String(serverBackupCustomEndpointFlag, "", "Server Backup API base URL, used in calls to this API") @@ -159,6 +168,10 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API") cmd.Flags().String(iaasCustomEndpointFlag, "", "IaaS API base URL, used in calls to this API") cmd.Flags().String(tokenCustomEndpointFlag, "", "Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication.") + cmd.Flags().String(intakeCustomEndpointFlag, "", "Intake API base URL, used in calls to this API") + cmd.Flags().String(logsCustomEndpointFlag, "", "Logs API base URL, used in calls to this API") + cmd.Flags().String(sfsCustomEndpointFlag, "", "SFS API base URL, used in calls to this API") + cmd.Flags().String(cdnCustomEndpointFlag, "", "CDN API base URL, used in calls to this API") err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag)) cobra.CheckErr(err) @@ -175,6 +188,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.DNSCustomEndpointKey, cmd.Flags().Lookup(dnsCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.EdgeCustomEndpointKey, cmd.Flags().Lookup(edgeCustomEndpointFlag)) + cobra.CheckErr(err) err = viper.BindPFlag(config.LoadBalancerCustomEndpointKey, cmd.Flags().Lookup(loadBalancerCustomEndpointFlag)) cobra.CheckErr(err) err = viper.BindPFlag(config.LogMeCustomEndpointKey, cmd.Flags().Lookup(logMeCustomEndpointFlag)) @@ -197,6 +212,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.SecretsManagerCustomEndpointKey, cmd.Flags().Lookup(secretsManagerCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.KMSCustomEndpointKey, cmd.Flags().Lookup(kmsCustomEndpointFlag)) + cobra.CheckErr(err) err = viper.BindPFlag(config.ServerBackupCustomEndpointKey, cmd.Flags().Lookup(serverBackupCustomEndpointFlag)) cobra.CheckErr(err) err = viper.BindPFlag(config.ServerOsUpdateCustomEndpointKey, cmd.Flags().Lookup(serverOsUpdateCustomEndpointFlag)) @@ -215,9 +232,17 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.TokenCustomEndpointKey, cmd.Flags().Lookup(tokenCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.IntakeCustomEndpointKey, cmd.Flags().Lookup(intakeCustomEndpointFlag)) + cobra.CheckErr(err) + err = viper.BindPFlag(config.LogsCustomEndpointKey, cmd.Flags().Lookup(logsCustomEndpointFlag)) + cobra.CheckErr(err) + err = viper.BindPFlag(config.SfsCustomEndpointKey, cmd.Flags().Lookup(sfsCustomEndpointFlag)) + cobra.CheckErr(err) + err = viper.BindPFlag(config.CDNCustomEndpointKey, cmd.Flags().Lookup(cdnCustomEndpointFlag)) + cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { sessionTimeLimit, err := parseSessionTimeLimit(p, cmd) if err != nil { return nil, &errors.FlagValidationError{ @@ -230,10 +255,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { // globalflags.Parse uses the flags, and fallsback to config file // To check if projectId was passed, we use the first rather than the second projectIdFromFlag := flags.FlagToStringPointer(p, cmd, globalflags.ProjectIdFlag) - projectIdSet := false - if projectIdFromFlag != nil { - projectIdSet = true - } + projectIdSet := projectIdFromFlag != nil allowedUrlDomainFromFlag := flags.FlagToStringPointer(p, cmd, allowedUrlDomainFlag) allowedUrlDomainFlagValue := flags.FlagToStringValue(p, cmd, allowedUrlDomainFlag) @@ -246,15 +268,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ProjectIdSet: projectIdSet, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/config/set/set_test.go b/internal/cmd/config/set/set_test.go index 8015fddff..c13c84d5c 100644 --- a/internal/cmd/config/set/set_test.go +++ b/internal/cmd/config/set/set_test.go @@ -3,19 +3,17 @@ package set import ( "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" ) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -123,46 +121,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index a288be6a3..580292b2e 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -3,7 +3,8 @@ package unset import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,11 +17,10 @@ import ( ) const ( - asyncFlag = globalflags.AsyncFlag - outputFormatFlag = globalflags.OutputFormatFlag - projectIdFlag = globalflags.ProjectIdFlag - regionFlag = globalflags.RegionFlag - verbosityFlag = globalflags.VerbosityFlag + asyncFlag = globalflags.AsyncFlag + projectIdFlag = globalflags.ProjectIdFlag + regionFlag = globalflags.RegionFlag + assumeYesFlag = globalflags.AssumeYesFlag sessionTimeLimitFlag = "session-time-limit" identityProviderCustomWellKnownConfigurationFlag = "identity-provider-custom-well-known-configuration" @@ -29,6 +29,7 @@ const ( authorizationCustomEndpointFlag = "authorization-custom-endpoint" dnsCustomEndpointFlag = "dns-custom-endpoint" + edgeCustomEndpointFlag = "edge-custom-endpoint" loadBalancerCustomEndpointFlag = "load-balancer-custom-endpoint" logMeCustomEndpointFlag = "logme-custom-endpoint" mariaDBCustomEndpointFlag = "mariadb-custom-endpoint" @@ -41,15 +42,25 @@ const ( redisCustomEndpointFlag = "redis-custom-endpoint" resourceManagerCustomEndpointFlag = "resource-manager-custom-endpoint" secretsManagerCustomEndpointFlag = "secrets-manager-custom-endpoint" + kmsCustomEndpointFlag = "kms-custom-endpoint" serviceAccountCustomEndpointFlag = "service-account-custom-endpoint" serviceEnablementCustomEndpointFlag = "service-enablement-custom-endpoint" serverBackupCustomEndpointFlag = "serverbackup-custom-endpoint" serverOsUpdateCustomEndpointFlag = "server-osupdate-custom-endpoint" runCommandCustomEndpointFlag = "runcommand-custom-endpoint" + sfsCustomEndpointFlag = "sfs-custom-endpoint" skeCustomEndpointFlag = "ske-custom-endpoint" sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" iaasCustomEndpointFlag = "iaas-custom-endpoint" tokenCustomEndpointFlag = "token-custom-endpoint" + intakeCustomEndpointFlag = "intake-custom-endpoint" + logsCustomEndpointFlag = "logs-custom-endpoint" + cdnCustomEndpointFlag = "cdn-custom-endpoint" +) + +var ( + outputFormatFlag = globalflags.OutputFormatFlag.Name() + verbosityFlag = globalflags.VerbosityFlag.Name() ) type inputModel struct { @@ -58,6 +69,7 @@ type inputModel struct { ProjectId bool Region bool Verbosity bool + AssumeYes bool SessionTimeLimit bool IdentityProviderCustomEndpoint bool @@ -66,6 +78,7 @@ type inputModel struct { AuthorizationCustomEndpoint bool DNSCustomEndpoint bool + EdgeCustomEndpoint bool LoadBalancerCustomEndpoint bool LogMeCustomEndpoint bool MariaDBCustomEndpoint bool @@ -78,18 +91,23 @@ type inputModel struct { RedisCustomEndpoint bool ResourceManagerCustomEndpoint bool SecretsManagerCustomEndpoint bool + KMSCustomEndpoint bool ServerBackupCustomEndpoint bool ServerOsUpdateCustomEndpoint bool RunCommandCustomEndpoint bool ServiceAccountCustomEndpoint bool ServiceEnablementCustomEndpoint bool + SfsCustomEndpoint bool SKECustomEndpoint bool SQLServerFlexCustomEndpoint bool IaaSCustomEndpoint bool TokenCustomEndpoint bool + IntakeCustomEndpoint bool + LogsCustomEndpoint bool + CDNCustomEndpoint bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "unset", Short: "Unsets CLI configuration options", @@ -124,6 +142,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if model.Verbosity { viper.Set(config.VerbosityKey, globalflags.VerbosityDefault) } + if model.AssumeYes { + viper.Set(config.AssumeYesKey, config.AssumeYesDefault) + } if model.SessionTimeLimit { viper.Set(config.SessionTimeLimitKey, config.SessionTimeLimitDefault) @@ -147,6 +168,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if model.DNSCustomEndpoint { viper.Set(config.DNSCustomEndpointKey, "") } + if model.EdgeCustomEndpoint { + viper.Set(config.EdgeCustomEndpointKey, "") + } if model.LoadBalancerCustomEndpoint { viper.Set(config.LoadBalancerCustomEndpointKey, "") } @@ -180,6 +204,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if model.SecretsManagerCustomEndpoint { viper.Set(config.SecretsManagerCustomEndpointKey, "") } + if model.KMSCustomEndpoint { + viper.Set(config.KMSCustomEndpointKey, "") + } if model.ServiceAccountCustomEndpoint { viper.Set(config.ServiceAccountCustomEndpointKey, "") } @@ -207,6 +234,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if model.TokenCustomEndpoint { viper.Set(config.TokenCustomEndpointKey, "") } + if model.IntakeCustomEndpoint { + viper.Set(config.IntakeCustomEndpointKey, "") + } + if model.LogsCustomEndpoint { + viper.Set(config.LogsCustomEndpointKey, "") + } + if model.SfsCustomEndpoint { + viper.Set(config.SfsCustomEndpointKey, "") + } + if model.CDNCustomEndpoint { + viper.Set(config.CDNCustomEndpointKey, "") + } err := config.Write() if err != nil { @@ -225,6 +264,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(regionFlag, false, "Region") cmd.Flags().Bool(outputFormatFlag, false, "Output format") cmd.Flags().Bool(verbosityFlag, false, "Verbosity of the CLI") + cmd.Flags().Bool(assumeYesFlag, false, "If set, skips all confirmation prompts") cmd.Flags().Bool(sessionTimeLimitFlag, false, fmt.Sprintf("Maximum time before authentication is required again. If unset, defaults to %s", config.SessionTimeLimitDefault)) cmd.Flags().Bool(identityProviderCustomWellKnownConfigurationFlag, false, "Identity Provider well-known OpenID configuration URL. If unset, uses the default identity provider") @@ -234,6 +274,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(observabilityCustomEndpointFlag, false, "Observability API base URL. If unset, uses the default base URL") cmd.Flags().Bool(authorizationCustomEndpointFlag, false, "Authorization API base URL. If unset, uses the default base URL") cmd.Flags().Bool(dnsCustomEndpointFlag, false, "DNS API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(edgeCustomEndpointFlag, false, "Edge API base URL. If unset, uses the default base URL") cmd.Flags().Bool(loadBalancerCustomEndpointFlag, false, "Load Balancer API base URL. If unset, uses the default base URL") cmd.Flags().Bool(logMeCustomEndpointFlag, false, "LogMe API base URL. If unset, uses the default base URL") cmd.Flags().Bool(mariaDBCustomEndpointFlag, false, "MariaDB API base URL. If unset, uses the default base URL") @@ -245,6 +286,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(redisCustomEndpointFlag, false, "Redis API base URL. If unset, uses the default base URL") cmd.Flags().Bool(resourceManagerCustomEndpointFlag, false, "Resource Manager API base URL. If unset, uses the default base URL") cmd.Flags().Bool(secretsManagerCustomEndpointFlag, false, "Secrets Manager API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(kmsCustomEndpointFlag, false, "KMS API base URL. If unset, uses the default base URL") cmd.Flags().Bool(serviceAccountCustomEndpointFlag, false, "Service Account API base URL. If unset, uses the default base URL") cmd.Flags().Bool(serviceEnablementCustomEndpointFlag, false, "Service Enablement API base URL. If unset, uses the default base URL") cmd.Flags().Bool(serverBackupCustomEndpointFlag, false, "Server Backup base URL. If unset, uses the default base URL") @@ -254,6 +296,10 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL") cmd.Flags().Bool(iaasCustomEndpointFlag, false, "IaaS API base URL. If unset, uses the default base URL") cmd.Flags().Bool(tokenCustomEndpointFlag, false, "Custom token endpoint of the Service Account API, which is used to request access tokens when the service account authentication is activated. Not relevant for user authentication.") + cmd.Flags().Bool(intakeCustomEndpointFlag, false, "Intake API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(logsCustomEndpointFlag, false, "Logs API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(sfsCustomEndpointFlag, false, "SFS API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(cdnCustomEndpointFlag, false, "Custom CDN endpoint URL. If unset, uses the default base URL") } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { @@ -263,6 +309,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { ProjectId: flags.FlagToBoolValue(p, cmd, projectIdFlag), Region: flags.FlagToBoolValue(p, cmd, regionFlag), Verbosity: flags.FlagToBoolValue(p, cmd, verbosityFlag), + AssumeYes: flags.FlagToBoolValue(p, cmd, assumeYesFlag), SessionTimeLimit: flags.FlagToBoolValue(p, cmd, sessionTimeLimitFlag), IdentityProviderCustomEndpoint: flags.FlagToBoolValue(p, cmd, identityProviderCustomWellKnownConfigurationFlag), @@ -271,6 +318,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { AuthorizationCustomEndpoint: flags.FlagToBoolValue(p, cmd, authorizationCustomEndpointFlag), DNSCustomEndpoint: flags.FlagToBoolValue(p, cmd, dnsCustomEndpointFlag), + EdgeCustomEndpoint: flags.FlagToBoolValue(p, cmd, edgeCustomEndpointFlag), LoadBalancerCustomEndpoint: flags.FlagToBoolValue(p, cmd, loadBalancerCustomEndpointFlag), LogMeCustomEndpoint: flags.FlagToBoolValue(p, cmd, logMeCustomEndpointFlag), MariaDBCustomEndpoint: flags.FlagToBoolValue(p, cmd, mariaDBCustomEndpointFlag), @@ -283,25 +331,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { RedisCustomEndpoint: flags.FlagToBoolValue(p, cmd, redisCustomEndpointFlag), ResourceManagerCustomEndpoint: flags.FlagToBoolValue(p, cmd, resourceManagerCustomEndpointFlag), SecretsManagerCustomEndpoint: flags.FlagToBoolValue(p, cmd, secretsManagerCustomEndpointFlag), + KMSCustomEndpoint: flags.FlagToBoolValue(p, cmd, kmsCustomEndpointFlag), ServiceAccountCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceAccountCustomEndpointFlag), ServiceEnablementCustomEndpoint: flags.FlagToBoolValue(p, cmd, serviceEnablementCustomEndpointFlag), ServerBackupCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverBackupCustomEndpointFlag), ServerOsUpdateCustomEndpoint: flags.FlagToBoolValue(p, cmd, serverOsUpdateCustomEndpointFlag), RunCommandCustomEndpoint: flags.FlagToBoolValue(p, cmd, runCommandCustomEndpointFlag), SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag), + SfsCustomEndpoint: flags.FlagToBoolValue(p, cmd, sfsCustomEndpointFlag), SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag), IaaSCustomEndpoint: flags.FlagToBoolValue(p, cmd, iaasCustomEndpointFlag), TokenCustomEndpoint: flags.FlagToBoolValue(p, cmd, tokenCustomEndpointFlag), + IntakeCustomEndpoint: flags.FlagToBoolValue(p, cmd, intakeCustomEndpointFlag), + LogsCustomEndpoint: flags.FlagToBoolValue(p, cmd, logsCustomEndpointFlag), + CDNCustomEndpoint: flags.FlagToBoolValue(p, cmd, cdnCustomEndpointFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model } diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 246696f15..17edcc9b7 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -4,11 +4,9 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool { @@ -17,6 +15,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool outputFormatFlag: true, projectIdFlag: true, verbosityFlag: true, + assumeYesFlag: true, sessionTimeLimitFlag: true, identityProviderCustomWellKnownConfigurationFlag: true, @@ -25,6 +24,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool authorizationCustomEndpointFlag: true, dnsCustomEndpointFlag: true, + edgeCustomEndpointFlag: true, loadBalancerCustomEndpointFlag: true, logMeCustomEndpointFlag: true, mariaDBCustomEndpointFlag: true, @@ -35,14 +35,19 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool redisCustomEndpointFlag: true, resourceManagerCustomEndpointFlag: true, secretsManagerCustomEndpointFlag: true, + kmsCustomEndpointFlag: true, serviceAccountCustomEndpointFlag: true, serverBackupCustomEndpointFlag: true, serverOsUpdateCustomEndpointFlag: true, runCommandCustomEndpointFlag: true, + sfsCustomEndpointFlag: true, skeCustomEndpointFlag: true, sqlServerFlexCustomEndpointFlag: true, iaasCustomEndpointFlag: true, tokenCustomEndpointFlag: true, + intakeCustomEndpointFlag: true, + logsCustomEndpointFlag: true, + cdnCustomEndpointFlag: true, } for _, mod := range mods { mod(flagValues) @@ -56,6 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { OutputFormat: true, ProjectId: true, Verbosity: true, + AssumeYes: true, SessionTimeLimit: true, IdentityProviderCustomEndpoint: true, @@ -64,6 +70,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { AuthorizationCustomEndpoint: true, DNSCustomEndpoint: true, + EdgeCustomEndpoint: true, LoadBalancerCustomEndpoint: true, LogMeCustomEndpoint: true, MariaDBCustomEndpoint: true, @@ -74,14 +81,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { RedisCustomEndpoint: true, ResourceManagerCustomEndpoint: true, SecretsManagerCustomEndpoint: true, + KMSCustomEndpoint: true, ServiceAccountCustomEndpoint: true, ServerBackupCustomEndpoint: true, ServerOsUpdateCustomEndpoint: true, RunCommandCustomEndpoint: true, + SfsCustomEndpoint: true, SKECustomEndpoint: true, SQLServerFlexCustomEndpoint: true, IaaSCustomEndpoint: true, TokenCustomEndpoint: true, + IntakeCustomEndpoint: true, + LogsCustomEndpoint: true, + CDNCustomEndpoint: true, } for _, mod := range mods { mod(model) @@ -111,6 +123,7 @@ func TestParseInput(t *testing.T) { model.OutputFormat = false model.ProjectId = false model.Verbosity = false + model.AssumeYes = false model.SessionTimeLimit = false model.IdentityProviderCustomEndpoint = false @@ -119,6 +132,7 @@ func TestParseInput(t *testing.T) { model.AuthorizationCustomEndpoint = false model.DNSCustomEndpoint = false + model.EdgeCustomEndpoint = false model.LoadBalancerCustomEndpoint = false model.LogMeCustomEndpoint = false model.MariaDBCustomEndpoint = false @@ -129,14 +143,19 @@ func TestParseInput(t *testing.T) { model.RedisCustomEndpoint = false model.ResourceManagerCustomEndpoint = false model.SecretsManagerCustomEndpoint = false + model.KMSCustomEndpoint = false model.ServiceAccountCustomEndpoint = false model.ServerBackupCustomEndpoint = false model.ServerOsUpdateCustomEndpoint = false model.RunCommandCustomEndpoint = false + model.SfsCustomEndpoint = false model.SKECustomEndpoint = false model.SQLServerFlexCustomEndpoint = false model.IaaSCustomEndpoint = false model.TokenCustomEndpoint = false + model.IntakeCustomEndpoint = false + model.LogsCustomEndpoint = false + model.CDNCustomEndpoint = false }), }, { @@ -209,6 +228,16 @@ func TestParseInput(t *testing.T) { model.DNSCustomEndpoint = false }), }, + { + description: "edge custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[edgeCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.EdgeCustomEndpoint = false + }), + }, { description: "secrets manager custom endpoint empty", flagValues: fixtureFlagValues(func(flagValues map[string]bool) { @@ -219,6 +248,16 @@ func TestParseInput(t *testing.T) { model.SecretsManagerCustomEndpoint = false }), }, + { + description: "kms custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[kmsCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.KMSCustomEndpoint = false + }), + }, { description: "service account custom endpoint empty", flagValues: fixtureFlagValues(func(flagValues map[string]bool) { @@ -229,6 +268,16 @@ func TestParseInput(t *testing.T) { model.ServiceAccountCustomEndpoint = false }), }, + { + description: "sfs custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[sfsCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SfsCustomEndpoint = false + }), + }, { description: "ske custom endpoint empty", flagValues: fixtureFlagValues(func(flagValues map[string]bool) { @@ -289,11 +338,31 @@ func TestParseInput(t *testing.T) { model.TokenCustomEndpoint = false }), }, + { + description: "logs custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[logsCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.LogsCustomEndpoint = false + }), + }, + { + description: "cdn custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[cdnCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.CDNCustomEndpoint = false + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) for flag, value := range tt.flagValues { stringBool := fmt.Sprintf("%v", value) @@ -314,7 +383,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model := parseInput(p, cmd) + model := parseInput(params.Printer, cmd) if !tt.isValid { t.Fatalf("did not fail on invalid input") diff --git a/internal/cmd/curl/curl.go b/internal/cmd/curl/curl.go index 15b79627a..3529872bc 100644 --- a/internal/cmd/curl/curl.go +++ b/internal/cmd/curl/curl.go @@ -11,7 +11,8 @@ import ( "strings" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -24,7 +25,6 @@ import ( ) const ( - requestMethodFlag = "request" headerFlag = "header" dataFlag = "data" includeResponseHeadersFlag = "include" @@ -36,6 +36,25 @@ const ( urlArg = "URL" ) +var requestMethodFlag = flags.StringEnumFlag( + "request", + []string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodOptions, + http.MethodTrace, + }, + "HTTP method, defaults to GET", + flags.StringEnumIgnoreCase[string](), + flags.StringEnumDefaultValue(http.MethodGet), + flags.StringEnumShortHand[string]("X"), +) + type inputModel struct { URL string RequestMethod string @@ -46,7 +65,7 @@ type inputModel struct { OutputFile *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("curl %s", urlArg), Short: "Executes an authenticated HTTP request to an endpoint", @@ -116,20 +135,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - requestMethodOptions := []string{ - http.MethodGet, - http.MethodHead, - http.MethodPost, - http.MethodPut, - http.MethodPatch, - http.MethodDelete, - http.MethodConnect, - http.MethodOptions, - http.MethodTrace, - } + requestMethodFlag.Register(cmd.Flags()) headerFlagUsage := `Custom headers to include in the request, can be specified multiple times. If the "Authorization" header is set, it will override the authentication provided by the CLI` - - cmd.Flags().VarP(flags.EnumFlag(true, "", requestMethodOptions...), requestMethodFlag, "X", "HTTP method, defaults to GET") cmd.Flags().StringSliceP(headerFlag, "H", []string{}, headerFlagUsage) cmd.Flags().Var(flags.ReadFromFileFlag(), dataFlag, `Content to include in the request body. Can be a string or a file path prefixed with "@"`) cmd.Flags().Bool(includeResponseHeadersFlag, false, "If set, response headers are added to the output") @@ -139,10 +146,7 @@ func configureFlags(cmd *cobra.Command) { func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { urlString := inputArgs[0] - requestMethod := flags.FlagToStringValue(p, cmd, requestMethodFlag) - if requestMethod == "" { - requestMethod = http.MethodGet - } + requestMethod := requestMethodFlag.Get() model := inputModel{ URL: urlString, @@ -154,15 +158,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu OutputFile: flags.FlagToStringPointer(p, cmd, outputFileFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -181,17 +177,10 @@ func getBearerToken(p *print.Printer) (string, error) { return "", &errors.SessionExpiredError{} } - accessToken, err := auth.GetAccessToken() - if err != nil { - return "", err - } - - accessTokenExpired, err := auth.TokenExpired(accessToken) + accessToken, err := auth.GetValidAccessToken(p) if err != nil { - return "", err - } - if accessTokenExpired { - return "", &errors.AccessTokenExpiredError{} + p.Debug(print.ErrorLevel, "get valid access token: %v", err) + return "", &errors.SessionExpiredError{} } return accessToken, nil diff --git a/internal/cmd/curl/curl_test.go b/internal/cmd/curl/curl_test.go index 3fbab3cf4..405f8cafc 100644 --- a/internal/cmd/curl/curl_test.go +++ b/internal/cmd/curl/curl_test.go @@ -13,10 +13,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) @@ -35,7 +36,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - requestMethodFlag: "post", + requestMethodFlag.Name(): "post", headerFlag: "Test-header-1: Test value 1", dataFlag: "data", includeResponseHeadersFlag: "true", @@ -142,7 +143,7 @@ func TestParseInput(t *testing.T) { description: "invalid method 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[requestMethodFlag] = "" + flagValues[requestMethodFlag.Name()] = "" }), isValid: false, }, @@ -150,7 +151,7 @@ func TestParseInput(t *testing.T) { description: "invalid method 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[requestMethodFlag] = "foo" + flagValues[requestMethodFlag.Name()] = "foo" }), isValid: false, }, @@ -158,7 +159,7 @@ func TestParseInput(t *testing.T) { description: "invalid method 3", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[requestMethodFlag] = " GET" + flagValues[requestMethodFlag.Name()] = " GET" }), isValid: false, }, @@ -166,7 +167,7 @@ func TestParseInput(t *testing.T) { description: "valid method 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[requestMethodFlag] = "put" + flagValues[requestMethodFlag.Name()] = "put" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -177,7 +178,7 @@ func TestParseInput(t *testing.T) { description: "valid method 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[requestMethodFlag] = "pAtCh" + flagValues[requestMethodFlag.Name()] = "pAtCh" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -216,8 +217,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + requestMethodFlag.Reset() + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -262,7 +264,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -447,11 +449,10 @@ func TestOutputResponse(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResponse(p, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResponse(params.Printer, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResponse() error = %v, wantErr %v", err, tt.wantErr) } if tt.args.model != nil { diff --git a/internal/cmd/dns/dns.go b/internal/cmd/dns/dns.go index ccba3b66d..216c02dac 100644 --- a/internal/cmd/dns/dns.go +++ b/internal/cmd/dns/dns.go @@ -3,14 +3,14 @@ package dns import ( recordset "github.com/stackitcloud/stackit-cli/internal/cmd/dns/record-set" "github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "dns", Short: "Provides functionality for DNS", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(zone.NewCmd(params)) cmd.AddCommand(recordset.NewCmd(params)) } diff --git a/internal/cmd/dns/record-set/create/create.go b/internal/cmd/dns/record-set/create/create.go index 045f6ce29..7221945c3 100644 --- a/internal/cmd/dns/record-set/create/create.go +++ b/internal/cmd/dns/record-set/create/create.go @@ -2,12 +2,15 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,9 +20,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/client" dnsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" ) const ( @@ -28,23 +28,29 @@ const ( nameFlag = "name" recordFlag = "record" ttlFlag = "ttl" - typeFlag = "type" defaultType = dns.CREATERECORDSETPAYLOADTYPE_A txtType = dns.CREATERECORDSETPAYLOADTYPE_TXT ) +var typeFlag = flags.StringEnumFlag( + "type", + dns.AllowedCreateRecordSetPayloadTypeEnumValues, + "Record type,", + flags.StringEnumDefaultValue(defaultType), +) + type inputModel struct { *globalflags.GlobalFlagModel ZoneId string Comment *string Name *string Records []string - TTL *int64 - Type dns.CreateRecordSetPayloadTypes + TTL *int32 + Type dns.CreateRecordSetPayloadType } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a DNS record set", @@ -55,9 +61,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a DNS record set with name "my-rr" with records "1.2.3.4" and "5.6.7.8" in zone with ID "xxx"`, "$ stackit dns record-set create --zone-id xxx --name my-rr --record 1.2.3.4 --record 5.6.7.8"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,18 +74,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) zoneLabel = model.ZoneId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a record set for zone %s?", zoneLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a record set for zone %s?", zoneLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -88,17 +92,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create DNS record set: %w", err) } - recordSetId := *resp.Rrset.Id + recordSetId := resp.Rrset.Id // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating record set") - _, err = wait.CreateRecordSetWaitHandler(ctx, apiClient, model.ProjectId, model.ZoneId, recordSetId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating record set", func() error { + _, err = wait.CreateRecordSetWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId, recordSetId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS record set creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, zoneLabel, resp) @@ -109,38 +113,31 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - var typeFlagOptions []string - for _, val := range dns.AllowedCreateRecordSetPayloadTypesEnumValues { - typeFlagOptions = append(typeFlagOptions, string(val)) - } - cmd.Flags().Var(flags.UUIDFlag(), zoneIdFlag, "Zone ID") cmd.Flags().String(commentFlag, "", "User comment") cmd.Flags().String(nameFlag, "", "Name of the record, should be compliant with RFC1035, Section 2.3.4") - cmd.Flags().Int64(ttlFlag, 0, "Time to live, if not provided defaults to the zone's default TTL") + cmd.Flags().Int32(ttlFlag, 0, "Time to live, if not provided defaults to the zone's default TTL") cmd.Flags().StringSlice(recordFlag, []string{}, "Records belonging to the record set") - cmd.Flags().Var(flags.EnumFlag(false, string(defaultType), typeFlagOptions...), typeFlag, fmt.Sprintf("Record type, one of %q", typeFlagOptions)) + typeFlag.Register(cmd.Flags()) err := flags.MarkFlagsRequired(cmd, zoneIdFlag, nameFlag, recordFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } - recordType := flags.FlagWithDefaultToStringValue(p, cmd, typeFlag) - model := inputModel{ GlobalFlagModel: globalFlags, ZoneId: flags.FlagToStringValue(p, cmd, zoneIdFlag), Comment: flags.FlagToStringPointer(p, cmd, commentFlag), Name: flags.FlagToStringPointer(p, cmd, nameFlag), Records: flags.FlagToStringSliceValue(p, cmd, recordFlag), - TTL: flags.FlagToInt64Pointer(p, cmd, ttlFlag), - Type: dns.CreateRecordSetPayloadTypes(recordType), + TTL: flags.FlagToInt32Pointer(p, cmd, ttlFlag), + Type: typeFlag.Get(), } if model.Type == txtType { @@ -157,31 +154,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiCreateRecordSetRequest { records := make([]dns.RecordPayload, 0) for _, r := range model.Records { - records = append(records, dns.RecordPayload{Content: utils.Ptr(r)}) + records = append(records, dns.RecordPayload{Content: r}) } - req := apiClient.CreateRecordSet(ctx, model.ProjectId, model.ZoneId) + req := apiClient.DefaultAPI.CreateRecordSet(ctx, model.ProjectId, model.ZoneId) req = req.CreateRecordSetPayload(dns.CreateRecordSetPayload{ Comment: model.Comment, - Name: model.Name, - Records: &records, + Name: *model.Name, + Records: records, Ttl: model.TTL, - Type: &model.Type, + Type: model.Type, }) return req } @@ -190,29 +179,12 @@ func outputResult(p *print.Printer, model *inputModel, zoneLabel string, resp *d if resp == nil { return fmt.Errorf("record set response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS record-set: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS record-set: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } - p.Outputf("%s record set for zone %s. Record set ID: %s\n", operationState, zoneLabel, utils.PtrString(resp.Rrset.Id)) + p.Outputf("%s record set for zone %s. Record set ID: %s\n", operationState, zoneLabel, resp.Rrset.Id) return nil - } + }) } diff --git a/internal/cmd/dns/record-set/create/create_test.go b/internal/cmd/dns/record-set/create/create_test.go index 789e46626..1f19400f5 100644 --- a/internal/cmd/dns/record-set/create/create_test.go +++ b/internal/cmd/dns/record-set/create/create_test.go @@ -9,37 +9,36 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() var recordTxtOver255Char = []string{ - "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo", - "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo", - "foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar", + strings.Repeat("a", 255), + strings.Repeat("a", 255), + strings.Repeat("a", 60), } func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, - commentFlag: "comment", - nameFlag: "example.com", - recordFlag: "1.1.1.1", - ttlFlag: "3600", - typeFlag: "SOA", // Non-default value + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, + commentFlag: "comment", + nameFlag: "example.com", + recordFlag: "1.1.1.1", + ttlFlag: "3600", + typeFlag.Name(): "SOA", // Non-default value } for _, mod := range mods { mod(flagValues) @@ -57,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Name: utils.Ptr("example.com"), Comment: utils.Ptr("comment"), Records: []string{"1.1.1.1"}, - TTL: utils.Ptr(int64(3600)), + TTL: utils.Ptr(int32(3600)), Type: "SOA", } for _, mod := range mods { @@ -67,15 +66,15 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiCreateRecordSetRequest)) dns.ApiCreateRecordSetRequest { - request := testClient.CreateRecordSet(testCtx, testProjectId, testZoneId) + request := testClient.DefaultAPI.CreateRecordSet(testCtx, testProjectId, testZoneId) request = request.CreateRecordSetPayload(dns.CreateRecordSetPayload{ - Name: utils.Ptr("example.com"), + Name: "example.com", Comment: utils.Ptr("comment"), - Records: &[]dns.RecordPayload{ - {Content: utils.Ptr("1.1.1.1")}, + Records: []dns.RecordPayload{ + {Content: "1.1.1.1"}, }, - Ttl: utils.Ptr(int64(3600)), - Type: dns.CREATERECORDSETPAYLOADTYPE_SOA.Ptr(), + Ttl: utils.Ptr(int32(3600)), + Type: dns.CREATERECORDSETPAYLOADTYPE_SOA, }) for _, mod := range mods { mod(&request) @@ -86,6 +85,7 @@ func fixtureRequest(mods ...func(request *dns.ApiCreateRecordSetRequest)) dns.Ap func TestParseInput(t *testing.T) { var tests = []struct { description string + argValues []string flagValues map[string]string recordFlagValues []string isValid bool @@ -105,10 +105,10 @@ func TestParseInput(t *testing.T) { { description: "required fields only", flagValues: map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, - nameFlag: "example.com", - recordFlag: "1.1.1.1", + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, + nameFlag: "example.com", + recordFlag: "1.1.1.1", }, isValid: true, expectedModel: &inputModel{ @@ -125,12 +125,12 @@ func TestParseInput(t *testing.T) { { description: "zero values", flagValues: map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, - commentFlag: "", - nameFlag: "", - recordFlag: "1.1.1.1", - ttlFlag: "0", + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, + commentFlag: "", + nameFlag: "", + recordFlag: "1.1.1.1", + ttlFlag: "0", }, isValid: true, expectedModel: &inputModel{ @@ -142,28 +142,28 @@ func TestParseInput(t *testing.T) { Name: utils.Ptr(""), Comment: utils.Ptr(""), Records: []string{"1.1.1.1"}, - TTL: utils.Ptr(int64(0)), + TTL: utils.Ptr(int32(0)), Type: defaultType, }, }, { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -205,7 +205,7 @@ func TestParseInput(t *testing.T) { { description: "type missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, typeFlag) + delete(flagValues, typeFlag.Name()) }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -215,14 +215,14 @@ func TestParseInput(t *testing.T) { { description: "type invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[typeFlag] = "" + flagValues[typeFlag.Name()] = "" }), isValid: false, }, { description: "type invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[typeFlag] = "a" + flagValues[typeFlag.Name()] = "a" }), isValid: false, }, @@ -247,7 +247,7 @@ func TestParseInput(t *testing.T) { { description: "TXT record with > 255 characters", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[typeFlag] = string(txtType) + flagValues[typeFlag.Name()] = string(txtType) flagValues[recordFlag] = strings.Join(recordTxtOver255Char, "") }), isValid: true, @@ -267,56 +267,9 @@ func TestParseInput(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.recordFlagValues { - err := cmd.Flags().Set(recordFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", recordFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + recordFlag: tt.recordFlagValues, + }, tt.isValid) }) } } @@ -344,13 +297,13 @@ func TestBuildRequest(t *testing.T) { Records: []string{"1.1.1.1"}, Type: defaultType, }, - expectedRequest: testClient.CreateRecordSet(testCtx, testProjectId, testZoneId). + expectedRequest: testClient.DefaultAPI.CreateRecordSet(testCtx, testProjectId, testZoneId). CreateRecordSetPayload(dns.CreateRecordSetPayload{ - Name: utils.Ptr("example.com"), - Records: &[]dns.RecordPayload{ - {Content: utils.Ptr("1.1.1.1")}, + Name: "example.com", + Records: []dns.RecordPayload{ + {Content: "1.1.1.1"}, }, - Type: utils.Ptr(defaultType), + Type: defaultType, }), }, } @@ -360,7 +313,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -390,16 +343,16 @@ func TestOutputResult(t *testing.T) { name: "only record set as argument", args: args{ model: fixtureInputModel(), - resp: &dns.RecordSetResponse{Rrset: &dns.RecordSet{}}, + resp: &dns.RecordSetResponse{Rrset: dns.RecordSet{}}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.zoneLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.zoneLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/record-set/delete/delete.go b/internal/cmd/dns/record-set/delete/delete.go index 015b8019d..85448ecbf 100644 --- a/internal/cmd/dns/record-set/delete/delete.go +++ b/internal/cmd/dns/record-set/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" ) const ( @@ -33,7 +34,7 @@ type inputModel struct { RecordSetId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", recordSetIdArg), Short: "Deletes a DNS record set", @@ -57,24 +58,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) zoneLabel = model.ZoneId } - recordSetLabel, err := dnsUtils.GetRecordSetName(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId) + recordSetLabel, err := dnsUtils.GetRecordSetName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId, model.RecordSetId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get record set name: %v", err) recordSetLabel = model.RecordSetId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete record set %s of zone %s? (This cannot be undone)", recordSetLabel, zoneLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete record set %s of zone %s? (This cannot be undone)", recordSetLabel, zoneLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -89,13 +88,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting record set") - _, err = wait.DeleteRecordSetWaitHandler(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting record set", func() error { + _, err = wait.DeleteRecordSetWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId, model.RecordSetId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS record set deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -131,19 +130,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu RecordSetId: recordSetId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiDeleteRecordSetRequest { - req := apiClient.DeleteRecordSet(ctx, model.ProjectId, model.ZoneId, model.RecordSetId) + req := apiClient.DefaultAPI.DeleteRecordSet(ctx, model.ProjectId, model.ZoneId, model.RecordSetId) return req } diff --git a/internal/cmd/dns/record-set/delete/delete_test.go b/internal/cmd/dns/record-set/delete/delete_test.go index 55b534fde..13b08f852 100644 --- a/internal/cmd/dns/record-set/delete/delete_test.go +++ b/internal/cmd/dns/record-set/delete/delete_test.go @@ -4,22 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() var testRecordSetId = uuid.NewString() @@ -36,8 +33,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, } for _, mod := range mods { mod(flagValues) @@ -61,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiDeleteRecordSetRequest)) dns.ApiDeleteRecordSetRequest { - request := testClient.DeleteRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId) + request := testClient.DefaultAPI.DeleteRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId) for _, mod := range mods { mod(&request) } @@ -105,7 +102,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +110,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +118,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -165,54 +162,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -237,7 +187,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/dns/record-set/describe/describe.go b/internal/cmd/dns/record-set/describe/describe.go index c7d5e8c52..6c7bde12e 100644 --- a/internal/cmd/dns/record-set/describe/describe.go +++ b/internal/cmd/dns/record-set/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { RecordSetId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", recordSetIdArg), Short: "Shows details of a DNS record set", @@ -69,7 +68,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } recordSet := resp.Rrset - return outputResult(params.Printer, model.OutputFormat, recordSet) + return outputResult(params.Printer, model.OutputFormat, &recordSet) }, } configureFlags(cmd) @@ -97,20 +96,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu RecordSetId: recordSetId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiGetRecordSetRequest { - req := apiClient.GetRecordSet(ctx, model.ProjectId, model.ZoneId, model.RecordSetId) + req := apiClient.DefaultAPI.GetRecordSet(ctx, model.ProjectId, model.ZoneId, model.RecordSetId) return req } @@ -118,40 +109,24 @@ func outputResult(p *print.Printer, outputFormat string, recordSet *dns.RecordSe if recordSet == nil { return fmt.Errorf("record set response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(recordSet, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS record set: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(recordSet, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS record set: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - recordsData := make([]string, 0, len(*recordSet.Records)) - for _, r := range *recordSet.Records { - recordsData = append(recordsData, *r.Content) + return p.OutputResult(outputFormat, recordSet, func() error { + recordsData := make([]string, 0, len(recordSet.Records)) + for _, r := range recordSet.Records { + recordsData = append(recordsData, r.Content) } recordsDataJoin := strings.Join(recordsData, ", ") table := tables.NewTable() - table.AddRow("ID", utils.PtrString(recordSet.Id)) + table.AddRow("ID", recordSet.Id) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(recordSet.Name)) + table.AddRow("NAME", recordSet.Name) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(recordSet.State)) + table.AddRow("STATE", recordSet.State) table.AddSeparator() - table.AddRow("TTL", utils.PtrString(recordSet.Ttl)) + table.AddRow("TTL", recordSet.Ttl) table.AddSeparator() - table.AddRow("TYPE", utils.PtrString(recordSet.Type)) + table.AddRow("TYPE", recordSet.Type) table.AddSeparator() table.AddRow("RECORDS DATA", recordsDataJoin) err := table.Display(p) @@ -160,5 +135,5 @@ func outputResult(p *print.Printer, outputFormat string, recordSet *dns.RecordSe } return nil - } + }) } diff --git a/internal/cmd/dns/record-set/describe/describe_test.go b/internal/cmd/dns/record-set/describe/describe_test.go index 55a032bd8..e0d3ce184 100644 --- a/internal/cmd/dns/record-set/describe/describe_test.go +++ b/internal/cmd/dns/record-set/describe/describe_test.go @@ -7,18 +7,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() var testRecordSetId = uuid.NewString() @@ -35,8 +34,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, } for _, mod := range mods { mod(flagValues) @@ -60,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiGetRecordSetRequest)) dns.ApiGetRecordSetRequest { - request := testClient.GetRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId) + request := testClient.DefaultAPI.GetRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId) for _, mod := range mods { mod(&request) } @@ -104,7 +103,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -112,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -120,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -164,54 +163,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +188,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -264,16 +216,16 @@ func TestOutputResult(t *testing.T) { { name: "only record set as argument", args: args{ - recordSet: &dns.RecordSet{Records: &[]dns.Record{}}, + recordSet: &dns.RecordSet{Records: []dns.Record{}}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.recordSet); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.recordSet); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/record-set/list/list.go b/internal/cmd/dns/record-set/list/list.go index a1b59553b..cb3c0ffc2 100644 --- a/internal/cmd/dns/record-set/list/list.go +++ b/internal/cmd/dns/record-set/list/list.go @@ -2,14 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" "math" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,25 +20,29 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/client" dnsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) const ( - activeFlag = "active" - inactiveFlag = "inactive" - zoneIdFlag = "zone-id" - deletedFlag = "deleted" - nameLikeFlag = "name-like" - orderByNameFlag = "order-by-name" - limitFlag = "limit" - pageSizeFlag = "page-size" + activeFlag = "active" + inactiveFlag = "inactive" + zoneIdFlag = "zone-id" + deletedFlag = "deleted" + nameLikeFlag = "name-like" + limitFlag = "limit" + pageSizeFlag = "page-size" defaultPage = 1 pageSizeDefault = 100 deleteSucceededState = "DELETE_SUCCEEDED" ) +var orderByNameFlag = flags.StringEnumFlag( + "order-by-name", + []string{"asc", "desc"}, + "Order by name,", + flags.StringEnumIgnoreCase[string](), +) + type inputModel struct { *globalflags.GlobalFlagModel @@ -51,7 +56,7 @@ type inputModel struct { PageSize int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists DNS record sets", @@ -74,9 +79,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List the deleted DNS record-sets for zone with ID "xxx"`, "$ stackit dns record-set list --zone-id xxx --deleted"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -88,20 +93,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Fetch record sets - recordSets, err := fetchRecordSets(ctx, model, apiClient) + recordSets, err := fetchRecordSets(ctx, model, apiClient.DefaultAPI) if err != nil { return err } - if len(recordSets) == 0 { - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) - zoneLabel = model.ZoneId - } - params.Printer.Info("No record sets found for zone %s matching the criteria\n", zoneLabel) - return nil + + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) + zoneLabel = model.ZoneId } - return outputResult(params.Printer, model.OutputFormat, recordSets) + + return outputResult(params.Printer, model.OutputFormat, zoneLabel, recordSets) }, } @@ -110,14 +113,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - orderByNameFlagOptions := []string{"asc", "desc"} - cmd.Flags().Var(flags.UUIDFlag(), zoneIdFlag, "Zone ID") cmd.Flags().Bool(activeFlag, false, "Filter for active record sets") cmd.Flags().Bool(inactiveFlag, false, "Filter for inactive record sets. Deleted record sets are always inactive and will be included when this flag is set") cmd.Flags().Bool(deletedFlag, false, "Filter for deleted record sets") cmd.Flags().String(nameLikeFlag, "", "Filter by name") - cmd.Flags().Var(flags.EnumFlag(true, "", orderByNameFlagOptions...), orderByNameFlag, fmt.Sprintf("Order by name, one of %q", orderByNameFlagOptions)) + orderByNameFlag.Register(cmd.Flags()) cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") cmd.Flags().Int64(pageSizeFlag, pageSizeDefault, "Number of items fetched in each API call. Does not affect the number of items in the command output") @@ -125,7 +126,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -160,20 +161,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Inactive: inactive, Deleted: flags.FlagToBoolValue(p, cmd, deletedFlag), NameLike: flags.FlagToStringPointer(p, cmd, nameLikeFlag), - OrderByName: flags.FlagToStringPointer(p, cmd, orderByNameFlag), + OrderByName: orderByNameFlag.Ptr(), Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), PageSize: pageSize, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -194,7 +187,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient dnsClient, p req = req.NameLike(*model.NameLike) } if model.OrderByName != nil { - req = req.OrderByName(strings.ToUpper(*model.OrderByName)) + req = req.OrderByName(dns.ListRecordSetsOrderByNameParameter(strings.ToUpper(*model.OrderByName))) } // check integer overflows @@ -230,7 +223,7 @@ func fetchRecordSets(ctx context.Context, model *inputModel, apiClient dnsClient if err != nil { return nil, fmt.Errorf("get DNS record sets: %w", err) } - respRecordSets := *resp.RrSets + respRecordSets := resp.RrSets if len(respRecordSets) == 0 { break } @@ -249,40 +242,28 @@ func fetchRecordSets(ctx context.Context, model *inputModel, apiClient dnsClient return recordSets, nil } -func outputResult(p *print.Printer, outputFormat string, recordSets []dns.RecordSet) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(recordSets, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS record set list: %w", err) +func outputResult(p *print.Printer, outputFormat, zoneLabel string, recordSets []dns.RecordSet) error { + return p.OutputResult(outputFormat, recordSets, func() error { + if len(recordSets) == 0 { + p.Outputf("No record sets found for zone %s matching the criteria\n", zoneLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(recordSets, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS record set list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATUS", "TTL", "TYPE", "RECORD DATA") for i := range recordSets { rs := recordSets[i] - recordData := make([]string, 0, len(*rs.Records)) - for _, r := range *rs.Records { - recordData = append(recordData, *r.Content) + recordData := make([]string, 0, len(rs.Records)) + for _, r := range rs.Records { + recordData = append(recordData, r.Content) } recordDataJoin := strings.Join(recordData, ", ") table.AddRow( - utils.PtrString(rs.Id), - utils.PtrString(rs.Name), - utils.PtrString(rs.State), - utils.PtrString(rs.Ttl), - utils.PtrString(rs.Type), + rs.Id, + rs.Name, + rs.State, + rs.Ttl, + rs.Type, recordDataJoin, ) } @@ -292,5 +273,5 @@ func outputResult(p *print.Printer, outputFormat string, recordSets []dns.Record } return nil - } + }) } diff --git a/internal/cmd/dns/record-set/list/list_test.go b/internal/cmd/dns/record-set/list/list_test.go index 122a2b29d..d3a806c56 100644 --- a/internal/cmd/dns/record-set/list/list_test.go +++ b/internal/cmd/dns/record-set/list/list_test.go @@ -11,29 +11,28 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, - nameLikeFlag: "some-pattern", - orderByNameFlag: "asc", + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, + nameLikeFlag: "some-pattern", + orderByNameFlag.Name(): "asc", } for _, mod := range mods { mod(flagValues) @@ -59,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiListRecordSetsRequest)) dns.ApiListRecordSetsRequest { - request := testClient.ListRecordSets(testCtx, testProjectId, testZoneId) + request := testClient.DefaultAPI.ListRecordSets(testCtx, testProjectId, testZoneId) request = request.NameLike("some-pattern") request = request.OrderByName("ASC") request = request.PageSize(pageSizeDefault) @@ -72,6 +71,7 @@ func fixtureRequest(mods ...func(request *dns.ApiListRecordSetsRequest)) dns.Api func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -128,8 +128,8 @@ func TestParseInput(t *testing.T) { { description: "required fields only", flagValues: map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, }, isValid: true, expectedModel: &inputModel{ @@ -144,21 +144,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -175,7 +175,7 @@ func TestParseInput(t *testing.T) { { description: "order by name desc", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[orderByNameFlag] = "desc" + flagValues[orderByNameFlag.Name()] = "desc" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -185,14 +185,14 @@ func TestParseInput(t *testing.T) { { description: "order by name invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[orderByNameFlag] = "" + flagValues[orderByNameFlag.Name()] = "" }), isValid: false, }, { description: "order by name invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[orderByNameFlag] = "invalid" + flagValues[orderByNameFlag.Name()] = "invalid" }), isValid: false, }, @@ -214,46 +214,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -312,16 +273,16 @@ func TestBuildRequest(t *testing.T) { PageSize: 10, }, page: 1, - expectedRequest: testClient.ListRecordSets(testCtx, testProjectId, testZoneId).Page(1).PageSize(10).StateNeq(deleteSucceededState), + expectedRequest: testClient.DefaultAPI.ListRecordSets(testCtx, testProjectId, testZoneId).Page(1).PageSize(10).StateNeq(deleteSucceededState), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient, tt.page) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI, tt.page) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -459,7 +420,7 @@ func TestFetchRecordSets(t *testing.T) { recordSets := make([]dns.RecordSet, numItemsToReturn) mockedResp := dns.ListRecordSetsResponse{ - RrSets: &recordSets, + RrSets: recordSets, } mockedRespBytes, err := json.Marshal(mockedResp) @@ -482,7 +443,7 @@ func TestFetchRecordSets(t *testing.T) { t.Fatalf("Failed to initialize client: %v", err) } - recordSets, err := fetchRecordSets(testCtx, tt.model, client) + recordSets, err := fetchRecordSets(testCtx, tt.model, client.DefaultAPI) if err != nil { if !tt.apiCallFails { t.Fatalf("did not fail on invalid input") @@ -505,6 +466,7 @@ func TestFetchRecordSets(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + zoneLabel string recordSets []dns.RecordSet } tests := []struct { @@ -518,11 +480,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.recordSets); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.zoneLabel, tt.args.recordSets); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/record-set/record_set.go b/internal/cmd/dns/record-set/record_set.go index d00152da0..548c66ee6 100644 --- a/internal/cmd/dns/record-set/record_set.go +++ b/internal/cmd/dns/record-set/record_set.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/dns/record-set/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/dns/record-set/list" "github.com/stackitcloud/stackit-cli/internal/cmd/dns/record-set/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "record-set", Short: "Provides functionality for DNS record set", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/dns/record-set/update/update.go b/internal/cmd/dns/record-set/update/update.go index 74630d455..749d0456a 100644 --- a/internal/cmd/dns/record-set/update/update.go +++ b/internal/cmd/dns/record-set/update/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" ) const ( @@ -39,11 +40,11 @@ type inputModel struct { Comment *string Name *string Records *[]string - TTL *int64 + TTL *int32 Type *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", recordSetIdArg), Short: "Updates a DNS record set", @@ -67,19 +68,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) zoneLabel = model.ZoneId } - recordSetLabel, err := dnsUtils.GetRecordSetName(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId) + recordSetLabel, err := dnsUtils.GetRecordSetName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId, model.RecordSetId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get record set name: %v", err) recordSetLabel = model.RecordSetId } - typeLabel, err := dnsUtils.GetRecordSetType(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId) + typeLabel, err := dnsUtils.GetRecordSetType(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId, model.RecordSetId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get record set type: %v", err) } @@ -92,12 +93,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update record set %s of zone %s?", recordSetLabel, zoneLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -109,13 +108,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating record set") - _, err = wait.PartialUpdateRecordSetWaitHandler(ctx, apiClient, model.ProjectId, model.ZoneId, model.RecordSetId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating record set", func() error { + _, err = wait.PartialUpdateRecordSetWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId, model.RecordSetId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS record set update: %w", err) } - s.Stop() } operationState := "Updated" @@ -134,7 +133,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), zoneIdFlag, "Zone ID") cmd.Flags().String(commentFlag, "", "User comment") cmd.Flags().String(nameFlag, "", "Name of the record, should be compliant with RFC1035, Section 2.3.4") - cmd.Flags().Int64(ttlFlag, 0, "Time to live, if not provided defaults to the zone's default TTL") + cmd.Flags().Int32(ttlFlag, 0, "Time to live, if not provided defaults to the zone's default TTL") cmd.Flags().StringSlice(recordFlag, []string{}, "Records belonging to the record set. If this flag is used, records already created that aren't set when running the command will be deleted") err := flags.MarkFlagsRequired(cmd, zoneIdFlag) @@ -153,7 +152,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu comment := flags.FlagToStringPointer(p, cmd, commentFlag) name := flags.FlagToStringPointer(p, cmd, nameFlag) records := flags.FlagToStringSlicePointer(p, cmd, recordFlag) - ttl := flags.FlagToInt64Pointer(p, cmd, ttlFlag) + ttl := flags.FlagToInt32Pointer(p, cmd, ttlFlag) if comment == nil && name == nil && records == nil && ttl == nil { return nil, &errors.EmptyUpdateError{} @@ -169,15 +168,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu TTL: ttl, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -203,15 +194,15 @@ func parseTxtRecord(records *[]string) error { } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiPartialUpdateRecordSetRequest { - var records *[]dns.RecordPayload = nil + var records []dns.RecordPayload = nil if model.Records != nil { - records = utils.Ptr(make([]dns.RecordPayload, 0)) + records = make([]dns.RecordPayload, 0) for _, r := range *model.Records { - records = utils.Ptr(append(*records, dns.RecordPayload{Content: utils.Ptr(r)})) + records = append(records, dns.RecordPayload{Content: r}) } } - req := apiClient.PartialUpdateRecordSet(ctx, model.ProjectId, model.ZoneId, model.RecordSetId) + req := apiClient.DefaultAPI.PartialUpdateRecordSet(ctx, model.ProjectId, model.ZoneId, model.RecordSetId) req = req.PartialUpdateRecordSetPayload(dns.PartialUpdateRecordSetPayload{ Comment: model.Comment, Name: model.Name, diff --git a/internal/cmd/dns/record-set/update/update_test.go b/internal/cmd/dns/record-set/update/update_test.go index 228b4b956..03586da3c 100644 --- a/internal/cmd/dns/record-set/update/update_test.go +++ b/internal/cmd/dns/record-set/update/update_test.go @@ -4,24 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() var testRecordSetId = uuid.NewString() @@ -45,12 +41,12 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, - commentFlag: "comment", - nameFlag: "example.com", - recordFlag: "1.1.1.1", - ttlFlag: "3600", + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, + commentFlag: "comment", + nameFlag: "example.com", + recordFlag: "1.1.1.1", + ttlFlag: "3600", } for _, mod := range mods { mod(flagValues) @@ -69,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Name: utils.Ptr("example.com"), Comment: utils.Ptr("comment"), Records: &[]string{"1.1.1.1"}, - TTL: utils.Ptr(int64(3600)), + TTL: utils.Ptr(int32(3600)), } for _, mod := range mods { mod(model) @@ -78,14 +74,14 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiPartialUpdateRecordSetRequest)) dns.ApiPartialUpdateRecordSetRequest { - request := testClient.PartialUpdateRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId) + request := testClient.DefaultAPI.PartialUpdateRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId) request = request.PartialUpdateRecordSetPayload(dns.PartialUpdateRecordSetPayload{ Name: utils.Ptr("example.com"), Comment: utils.Ptr("comment"), - Records: &[]dns.RecordPayload{ - {Content: utils.Ptr("1.1.1.1")}, + Records: []dns.RecordPayload{ + {Content: "1.1.1.1"}, }, - Ttl: utils.Ptr(int64(3600)), + Ttl: utils.Ptr(int32(3600)), }) req := &request for _, mod := range mods { @@ -132,8 +128,8 @@ func TestParseInput(t *testing.T) { description: "required flags only (no values to update)", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, }, isValid: false, expectedModel: &inputModel{ @@ -149,12 +145,12 @@ func TestParseInput(t *testing.T) { description: "zero values", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, - zoneIdFlag: testZoneId, - commentFlag: "", - nameFlag: "", - recordFlag: "1.1.1.1", - ttlFlag: "0", + globalflags.ProjectIdFlag: testProjectId, + zoneIdFlag: testZoneId, + commentFlag: "", + nameFlag: "", + recordFlag: "1.1.1.1", + ttlFlag: "0", }, isValid: true, expectedModel: &inputModel{ @@ -167,14 +163,14 @@ func TestParseInput(t *testing.T) { Name: utils.Ptr(""), Comment: utils.Ptr(""), Records: &[]string{"1.1.1.1"}, - TTL: utils.Ptr(int64(0)), + TTL: utils.Ptr(int32(0)), }, }, { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -182,7 +178,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -190,7 +186,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -254,8 +250,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -297,7 +293,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -402,7 +398,7 @@ func TestBuildRequest(t *testing.T) { ZoneId: testZoneId, RecordSetId: testRecordSetId, }, - expectedRequest: testClient.PartialUpdateRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId). + expectedRequest: testClient.DefaultAPI.PartialUpdateRecordSet(testCtx, testProjectId, testZoneId, testRecordSetId). PartialUpdateRecordSetPayload(dns.PartialUpdateRecordSetPayload{}), }, } @@ -412,7 +408,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/dns/zone/clone/clone.go b/internal/cmd/dns/zone/clone/clone.go index 7d81f13f8..e56e9de89 100644 --- a/internal/cmd/dns/zone/clone/clone.go +++ b/internal/cmd/dns/zone/clone/clone.go @@ -2,11 +2,10 @@ package clone import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" ) const ( @@ -40,7 +39,7 @@ type inputModel struct { ZoneId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("clone %s", zoneIdArg), Short: "Clones a DNS zone", @@ -70,18 +69,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) zoneLabel = model.ZoneId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to clone the zone %q?", zoneLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to clone the zone %q?", zoneLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -90,17 +87,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("clone DNS zone: %w", err) } - zoneId := *resp.Zone.Id + zoneId := resp.Zone.Id // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Cloning zone") - _, err = wait.CreateZoneWaitHandler(ctx, apiClient, model.ProjectId, zoneId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Cloning zone", func() error { + _, err = wait.CreateZoneWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, zoneId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS zone cloning: %w", err) } - s.Stop() } return outputResult(params.Printer, model, zoneLabel, resp) @@ -137,23 +134,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ZoneId: zoneId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiCloneZoneRequest { - req := apiClient.CloneZone(ctx, model.ProjectId, model.ZoneId) + req := apiClient.DefaultAPI.CloneZone(ctx, model.ProjectId, model.ZoneId) req = req.CloneZonePayload(dns.CloneZonePayload{ Name: model.Name, - DnsName: model.DnsName, + DnsName: *model.DnsName, Description: model.Description, AdjustRecords: model.AdjustRecords, }) @@ -164,29 +153,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp if resp == nil { return fmt.Errorf("dns zone response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS zone: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS zone: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Cloned" if model.Async { operationState = "Triggered cloning of" } - p.Outputf("%s zone for project %q. Zone ID: %s\n", operationState, projectLabel, utils.PtrString(resp.Zone.Id)) + p.Outputf("%s zone for project %q. Zone ID: %s\n", operationState, projectLabel, resp.Zone.Id) return nil - } + }) } diff --git a/internal/cmd/dns/zone/clone/clone_test.go b/internal/cmd/dns/zone/clone/clone_test.go index 01f738f3a..7490434f7 100644 --- a/internal/cmd/dns/zone/clone/clone_test.go +++ b/internal/cmd/dns/zone/clone/clone_test.go @@ -7,19 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() @@ -35,11 +34,11 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "example", - dnsNameFlag: "example.com", - descriptionFlag: "Example", - adjustRecordsFlag: "false", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "example", + dnsNameFlag: "example.com", + descriptionFlag: "Example", + adjustRecordsFlag: "false", } for _, mod := range mods { mod(flagValues) @@ -66,10 +65,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiCloneZoneRequest)) dns.ApiCloneZoneRequest { - request := testClient.CloneZone(testCtx, testProjectId, testZoneId) + request := testClient.DefaultAPI.CloneZone(testCtx, testProjectId, testZoneId) request = request.CloneZonePayload(dns.CloneZonePayload{ Name: utils.Ptr("example"), - DnsName: utils.Ptr("example.com"), + DnsName: "example.com", Description: utils.Ptr("Example"), AdjustRecords: utils.Ptr(false), }) @@ -116,8 +115,8 @@ func TestParseInput(t *testing.T) { description: "required fields only", argValues: []string{testZoneId}, flagValues: map[string]string{ - projectIdFlag: testProjectId, - dnsNameFlag: "example.com", + globalflags.ProjectIdFlag: testProjectId, + dnsNameFlag: "example.com", }, isValid: true, expectedModel: &inputModel{ @@ -133,7 +132,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -141,7 +140,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -149,7 +148,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -169,54 +168,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -244,9 +196,9 @@ func TestBuildRequest(t *testing.T) { DnsName: utils.Ptr("example.com"), ZoneId: testZoneId, }, - expectedRequest: testClient.CloneZone(testCtx, testProjectId, testZoneId). + expectedRequest: testClient.DefaultAPI.CloneZone(testCtx, testProjectId, testZoneId). CloneZonePayload(dns.CloneZonePayload{ - DnsName: utils.Ptr("example.com"), + DnsName: "example.com", }), }, } @@ -256,7 +208,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -286,16 +238,16 @@ func TestOutputResult(t *testing.T) { name: "only zone response as argument", args: args{ model: fixtureInputModel(), - resp: &dns.ZoneResponse{Zone: &dns.Zone{}}, + resp: &dns.ZoneResponse{Zone: dns.Zone{}}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/zone/create/create.go b/internal/cmd/dns/zone/create/create.go index 081602f18..0951edda6 100644 --- a/internal/cmd/dns/zone/create/create.go +++ b/internal/cmd/dns/zone/create/create.go @@ -2,11 +2,8 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,11 +13,11 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" ) const ( @@ -29,7 +26,6 @@ const ( defaultTTLFlag = "default-ttl" primaryFlag = "primary" aclFlag = "acl" - typeFlag = "type" retryTimeFlag = "retry-time" refreshTimeFlag = "refresh-time" negativeCacheFlag = "negative-cache" @@ -39,24 +35,31 @@ const ( contactEmailFlag = "contact-email" ) +var typeFlag = flags.StringEnumFlag( + "type", + append(dns.AllowedCreateZonePayloadTypeEnumValues, ""), + "Zone type,", + flags.StringEnumDefaultValue(dns.CreateZonePayloadType("")), +) + type inputModel struct { *globalflags.GlobalFlagModel - Name *string - DnsName *string - DefaultTTL *int64 - Primaries *[]string + Name string + DnsName string + DefaultTTL *int32 + Primaries []string Acl *string - Type *dns.CreateZonePayloadTypes - RetryTime *int64 - RefreshTime *int64 - NegativeCache *int64 + Type *dns.CreateZonePayloadType + RetryTime *int32 + RefreshTime *int32 + NegativeCache *int32 IsReverseZone *bool - ExpireTime *int64 + ExpireTime *int32 Description *string ContactEmail *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a DNS zone", @@ -70,9 +73,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a DNS zone with name "my-zone", DNS name "www.my-zone.com" and default time to live of 1000ms`, "$ stackit dns zone create --name my-zone --dns-name www.my-zone.com --default-ttl 1000"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -89,12 +92,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a zone for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a zone for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -103,17 +104,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create DNS zone: %w", err) } - zoneId := *resp.Zone.Id + zoneId := resp.Zone.Id // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating zone") - _, err = wait.CreateZoneWaitHandler(ctx, apiClient, model.ProjectId, zoneId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating zone", func() error { + _, err = wait.CreateZoneWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, zoneId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS zone creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, resp) @@ -124,22 +125,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - var typeFlagOptions []string - for _, val := range dns.AllowedCreateZonePayloadTypesEnumValues { - typeFlagOptions = append(typeFlagOptions, string(val)) - } - cmd.Flags().String(nameFlag, "", "User given name of the zone") cmd.Flags().String(dnsNameFlag, "", "Fully qualified domain name of the DNS zone") - cmd.Flags().Int64(defaultTTLFlag, 1000, "Default time to live") + cmd.Flags().Int32(defaultTTLFlag, 1000, "Default time to live") cmd.Flags().StringSlice(primaryFlag, []string{}, "Primary name server for secondary zone") cmd.Flags().String(aclFlag, "", "Access control list") - cmd.Flags().Var(flags.EnumFlag(false, "", append(typeFlagOptions, "")...), typeFlag, fmt.Sprintf("Zone type, one of: %q", typeFlagOptions)) - cmd.Flags().Int64(retryTimeFlag, 0, "Retry time") - cmd.Flags().Int64(refreshTimeFlag, 0, "Refresh time") - cmd.Flags().Int64(negativeCacheFlag, 0, "Negative cache") + typeFlag.Register(cmd.Flags()) + cmd.Flags().Int32(retryTimeFlag, 0, "Retry time") + cmd.Flags().Int32(refreshTimeFlag, 0, "Refresh time") + cmd.Flags().Int32(negativeCacheFlag, 0, "Negative cache") cmd.Flags().Bool(isReverseZoneFlag, false, "Is reverse zone") - cmd.Flags().Int64(expireTimeFlag, 0, "Expire time") + cmd.Flags().Int32(expireTimeFlag, 0, "Expire time") cmd.Flags().String(descriptionFlag, "", "Description of the zone") cmd.Flags().String(contactEmailFlag, "", "Contact email for the zone") @@ -147,48 +143,40 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } - var zoneType *dns.CreateZonePayloadTypes - if zoneTypeString := flags.FlagToStringPointer(p, cmd, typeFlag); zoneTypeString != nil && *zoneTypeString != "" { - zoneType = dns.CreateZonePayloadTypes(*zoneTypeString).Ptr() + var zoneType *dns.CreateZonePayloadType + if typeFlagValue := typeFlag.Ptr(); typeFlagValue != nil && *typeFlagValue != "" { + zoneType = typeFlagValue } model := inputModel{ GlobalFlagModel: globalFlags, - Name: flags.FlagToStringPointer(p, cmd, nameFlag), - DnsName: flags.FlagToStringPointer(p, cmd, dnsNameFlag), - DefaultTTL: flags.FlagToInt64Pointer(p, cmd, defaultTTLFlag), - Primaries: flags.FlagToStringSlicePointer(p, cmd, primaryFlag), + Name: flags.FlagToStringValue(p, cmd, nameFlag), + DnsName: flags.FlagToStringValue(p, cmd, dnsNameFlag), + DefaultTTL: flags.FlagToInt32Pointer(p, cmd, defaultTTLFlag), + Primaries: flags.FlagToStringSliceValue(p, cmd, primaryFlag), Acl: flags.FlagToStringPointer(p, cmd, aclFlag), Type: zoneType, - RetryTime: flags.FlagToInt64Pointer(p, cmd, retryTimeFlag), - RefreshTime: flags.FlagToInt64Pointer(p, cmd, refreshTimeFlag), - NegativeCache: flags.FlagToInt64Pointer(p, cmd, negativeCacheFlag), + RetryTime: flags.FlagToInt32Pointer(p, cmd, retryTimeFlag), + RefreshTime: flags.FlagToInt32Pointer(p, cmd, refreshTimeFlag), + NegativeCache: flags.FlagToInt32Pointer(p, cmd, negativeCacheFlag), IsReverseZone: flags.FlagToBoolPointer(p, cmd, isReverseZoneFlag), - ExpireTime: flags.FlagToInt64Pointer(p, cmd, expireTimeFlag), + ExpireTime: flags.FlagToInt32Pointer(p, cmd, expireTimeFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), ContactEmail: flags.FlagToStringPointer(p, cmd, contactEmailFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiCreateZoneRequest { - req := apiClient.CreateZone(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreateZone(ctx, model.ProjectId) req = req.CreateZonePayload(dns.CreateZonePayload{ Name: model.Name, DnsName: model.DnsName, @@ -211,29 +199,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp if resp == nil { return fmt.Errorf("dns zone response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS zone: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS zone: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } - p.Outputf("%s zone for project %q. Zone ID: %s\n", operationState, projectLabel, utils.PtrString(resp.Zone.Id)) + p.Outputf("%s zone for project %q. Zone ID: %s\n", operationState, projectLabel, resp.Zone.Id) return nil - } + }) } diff --git a/internal/cmd/dns/zone/create/create_test.go b/internal/cmd/dns/zone/create/create_test.go index 536711d80..aee805cac 100644 --- a/internal/cmd/dns/zone/create/create_test.go +++ b/internal/cmd/dns/zone/create/create_test.go @@ -7,37 +7,36 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "example", - dnsNameFlag: "example.com", - defaultTTLFlag: "3600", - aclFlag: "0.0.0.0/0", - typeFlag: string(dns.CREATEZONEPAYLOADTYPE_PRIMARY), - primaryFlag: "1.1.1.1", - retryTimeFlag: "600", - refreshTimeFlag: "3600", - negativeCacheFlag: "60", - isReverseZoneFlag: "false", - expireTimeFlag: "36000000", - descriptionFlag: "Example", - contactEmailFlag: "example@example.com", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "example", + dnsNameFlag: "example.com", + defaultTTLFlag: "3600", + aclFlag: "0.0.0.0/0", + typeFlag.Name(): string(dns.CREATEZONEPAYLOADTYPE_PRIMARY), + primaryFlag: "1.1.1.1", + retryTimeFlag: "600", + refreshTimeFlag: "3600", + negativeCacheFlag: "60", + isReverseZoneFlag: "false", + expireTimeFlag: "36000000", + descriptionFlag: "Example", + contactEmailFlag: "example@example.com", } for _, mod := range mods { mod(flagValues) @@ -51,17 +50,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - Name: utils.Ptr("example"), - DnsName: utils.Ptr("example.com"), - DefaultTTL: utils.Ptr(int64(3600)), - Primaries: utils.Ptr([]string{"1.1.1.1"}), + Name: "example", + DnsName: "example.com", + DefaultTTL: utils.Ptr(int32(3600)), + Primaries: []string{"1.1.1.1"}, Acl: utils.Ptr("0.0.0.0/0"), Type: dns.CREATEZONEPAYLOADTYPE_PRIMARY.Ptr(), - RetryTime: utils.Ptr(int64(600)), - RefreshTime: utils.Ptr(int64(3600)), - NegativeCache: utils.Ptr(int64(60)), + RetryTime: utils.Ptr(int32(600)), + RefreshTime: utils.Ptr(int32(3600)), + NegativeCache: utils.Ptr(int32(60)), IsReverseZone: utils.Ptr(false), - ExpireTime: utils.Ptr(int64(36000000)), + ExpireTime: utils.Ptr(int32(36000000)), Description: utils.Ptr("Example"), ContactEmail: utils.Ptr("example@example.com"), } @@ -72,19 +71,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiCreateZoneRequest)) dns.ApiCreateZoneRequest { - request := testClient.CreateZone(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateZone(testCtx, testProjectId) request = request.CreateZonePayload(dns.CreateZonePayload{ - Name: utils.Ptr("example"), - DnsName: utils.Ptr("example.com"), - DefaultTTL: utils.Ptr(int64(3600)), - Primaries: utils.Ptr([]string{"1.1.1.1"}), + Name: "example", + DnsName: "example.com", + DefaultTTL: utils.Ptr(int32(3600)), + Primaries: []string{"1.1.1.1"}, Acl: utils.Ptr("0.0.0.0/0"), Type: dns.CREATEZONEPAYLOADTYPE_PRIMARY.Ptr(), - RetryTime: utils.Ptr(int64(600)), - RefreshTime: utils.Ptr(int64(3600)), - NegativeCache: utils.Ptr(int64(60)), + RetryTime: utils.Ptr(int32(600)), + RefreshTime: utils.Ptr(int32(3600)), + NegativeCache: utils.Ptr(int32(60)), IsReverseZone: utils.Ptr(false), - ExpireTime: utils.Ptr(int64(36000000)), + ExpireTime: utils.Ptr(int32(36000000)), Description: utils.Ptr("Example"), ContactEmail: utils.Ptr("example@example.com"), }) @@ -97,6 +96,7 @@ func fixtureRequest(mods ...func(request *dns.ApiCreateZoneRequest)) dns.ApiCrea func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string primaryFlagValues []string isValid bool @@ -116,9 +116,9 @@ func TestParseInput(t *testing.T) { { description: "required fields only", flagValues: map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "example", - dnsNameFlag: "example.com", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "example", + dnsNameFlag: "example.com", }, isValid: true, expectedModel: &inputModel{ @@ -126,26 +126,26 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - Name: utils.Ptr("example"), - DnsName: utils.Ptr("example.com"), + Name: "example", + DnsName: "example.com", }, }, { description: "zero values", flagValues: map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "", - dnsNameFlag: "", - defaultTTLFlag: "0", - aclFlag: "", - typeFlag: "", - retryTimeFlag: "0", - refreshTimeFlag: "0", - negativeCacheFlag: "0", - isReverseZoneFlag: "false", - expireTimeFlag: "0", - descriptionFlag: "", - contactEmailFlag: "", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "", + dnsNameFlag: "", + defaultTTLFlag: "0", + aclFlag: "", + typeFlag.Name(): "", + retryTimeFlag: "0", + refreshTimeFlag: "0", + negativeCacheFlag: "0", + isReverseZoneFlag: "false", + expireTimeFlag: "0", + descriptionFlag: "", + contactEmailFlag: "", }, isValid: true, expectedModel: &inputModel{ @@ -153,17 +153,17 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - Name: utils.Ptr(""), - DnsName: utils.Ptr(""), - DefaultTTL: utils.Ptr(int64(0)), + Name: "", + DnsName: "", + DefaultTTL: utils.Ptr(int32(0)), Primaries: nil, Acl: utils.Ptr(""), Type: nil, - RetryTime: utils.Ptr(int64(0)), - RefreshTime: utils.Ptr(int64(0)), - NegativeCache: utils.Ptr(int64(0)), + RetryTime: utils.Ptr(int32(0)), + RefreshTime: utils.Ptr(int32(0)), + NegativeCache: utils.Ptr(int32(0)), IsReverseZone: utils.Ptr(false), - ExpireTime: utils.Ptr(int64(0)), + ExpireTime: utils.Ptr(int32(0)), Description: utils.Ptr(""), ContactEmail: utils.Ptr(""), }, @@ -171,21 +171,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -195,9 +195,8 @@ func TestParseInput(t *testing.T) { primaryFlagValues: []string{"1.2.3.4", "5.6.7.8"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Primaries = utils.Ptr( - append(*model.Primaries, "1.2.3.4", "5.6.7.8"), - ) + model.Primaries = + append(model.Primaries, "1.2.3.4", "5.6.7.8") }), }, { @@ -206,65 +205,17 @@ func TestParseInput(t *testing.T) { primaryFlagValues: []string{"1.2.3.4,5.6.7.8"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Primaries = utils.Ptr( - append(*model.Primaries, "1.2.3.4", "5.6.7.8"), - ) + model.Primaries = + append(model.Primaries, "1.2.3.4", "5.6.7.8") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.primaryFlagValues { - err := cmd.Flags().Set(primaryFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", primaryFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + primaryFlag: tt.primaryFlagValues, + }, tt.isValid) }) } } @@ -287,13 +238,13 @@ func TestBuildRequest(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - Name: utils.Ptr("example"), - DnsName: utils.Ptr("example.com"), + Name: "example", + DnsName: "example.com", }, - expectedRequest: testClient.CreateZone(testCtx, testProjectId). + expectedRequest: testClient.DefaultAPI.CreateZone(testCtx, testProjectId). CreateZonePayload(dns.CreateZonePayload{ - Name: utils.Ptr("example"), - DnsName: utils.Ptr("example.com"), + Name: "example", + DnsName: "example.com", }), }, } @@ -303,7 +254,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -333,16 +284,16 @@ func TestOutputResult(t *testing.T) { name: "only zone response as argument", args: args{ model: fixtureInputModel(), - resp: &dns.ZoneResponse{Zone: &dns.Zone{}}, + resp: &dns.ZoneResponse{Zone: dns.Zone{}}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/zone/delete/delete.go b/internal/cmd/dns/zone/delete/delete.go index 443bbf186..a299dd074 100644 --- a/internal/cmd/dns/zone/delete/delete.go +++ b/internal/cmd/dns/zone/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { ZoneId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", zoneIdArg), Short: "Deletes a DNS zone", @@ -53,17 +54,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) zoneLabel = model.ZoneId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete zone %q? (This cannot be undone)", zoneLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete zone %q? (This cannot be undone)", zoneLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -78,13 +77,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting zone") - _, err = wait.DeleteZoneWaitHandler(ctx, apiClient, model.ProjectId, model.ZoneId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting zone", func() error { + _, err = wait.DeleteZoneWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS zone deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -110,19 +109,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ZoneId: zoneId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiDeleteZoneRequest { - req := apiClient.DeleteZone(ctx, model.ProjectId, model.ZoneId) + req := apiClient.DefaultAPI.DeleteZone(ctx, model.ProjectId, model.ZoneId) return req } diff --git a/internal/cmd/dns/zone/delete/delete_test.go b/internal/cmd/dns/zone/delete/delete_test.go index 77cb37649..51f5ec826 100644 --- a/internal/cmd/dns/zone/delete/delete_test.go +++ b/internal/cmd/dns/zone/delete/delete_test.go @@ -4,22 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() @@ -35,7 +32,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, } for _, mod := range mods { mod(flagValues) @@ -58,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiDeleteZoneRequest)) dns.ApiDeleteZoneRequest { - request := testClient.DeleteZone(testCtx, testProjectId, testZoneId) + request := testClient.DefaultAPI.DeleteZone(testCtx, testProjectId, testZoneId) for _, mod := range mods { mod(&request) } @@ -102,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +135,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -210,7 +160,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/dns/zone/describe/describe.go b/internal/cmd/dns/zone/describe/describe.go index d68bbb177..66a28b029 100644 --- a/internal/cmd/dns/zone/describe/describe.go +++ b/internal/cmd/dns/zone/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) const ( @@ -29,7 +28,7 @@ type inputModel struct { ZoneId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", zoneIdArg), Short: "Shows details of a DNS zone", @@ -63,7 +62,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } zone := resp.Zone - return outputResult(params.Printer, model.OutputFormat, zone) + return outputResult(params.Printer, model.OutputFormat, &zone) }, } return cmd @@ -82,20 +81,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ZoneId: zoneId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiGetZoneRequest { - req := apiClient.GetZone(ctx, model.ProjectId, model.ZoneId) + req := apiClient.DefaultAPI.GetZone(ctx, model.ProjectId, model.ZoneId) return req } @@ -103,36 +94,20 @@ func outputResult(p *print.Printer, outputFormat string, zone *dns.Zone) error { if zone == nil { return fmt.Errorf("zone response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(zone, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS zone: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(zone, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS zone: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, zone, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(zone.Id)) + table.AddRow("ID", zone.Id) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(zone.Name)) + table.AddRow("NAME", zone.Name) table.AddSeparator() table.AddRow("DESCRIPTION", utils.PtrString(zone.Description)) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(zone.State)) + table.AddRow("STATE", zone.State) table.AddSeparator() - table.AddRow("TYPE", utils.PtrString(zone.Type)) + table.AddRow("TYPE", zone.Type) table.AddSeparator() - table.AddRow("DNS NAME", utils.PtrString(zone.DnsName)) + table.AddRow("DNS NAME", zone.DnsName) table.AddSeparator() table.AddRow("REVERSE ZONE", utils.PtrString(zone.IsReverseZone)) table.AddSeparator() @@ -140,22 +115,22 @@ func outputResult(p *print.Printer, outputFormat string, zone *dns.Zone) error { table.AddSeparator() table.AddRow("CONTACT EMAIL", utils.PtrString(zone.ContactEmail)) table.AddSeparator() - table.AddRow("DEFAULT TTL", utils.PtrString(zone.DefaultTTL)) + table.AddRow("DEFAULT TTL", zone.DefaultTTL) table.AddSeparator() - table.AddRow("SERIAL NUMBER", utils.PtrString(zone.SerialNumber)) + table.AddRow("SERIAL NUMBER", zone.SerialNumber) table.AddSeparator() - table.AddRow("REFRESH TIME", utils.PtrString(zone.RefreshTime)) + table.AddRow("REFRESH TIME", zone.RefreshTime) table.AddSeparator() - table.AddRow("RETRY TIME", utils.PtrString(zone.RetryTime)) + table.AddRow("RETRY TIME", zone.RetryTime) table.AddSeparator() - table.AddRow("EXPIRE TIME", utils.PtrString(zone.ExpireTime)) + table.AddRow("EXPIRE TIME", zone.ExpireTime) table.AddSeparator() - table.AddRow("NEGATIVE CACHE", utils.PtrString(zone.NegativeCache)) + table.AddRow("NEGATIVE CACHE", zone.NegativeCache) err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/dns/zone/describe/describe_test.go b/internal/cmd/dns/zone/describe/describe_test.go index ed58cbb14..fbf60363b 100644 --- a/internal/cmd/dns/zone/describe/describe_test.go +++ b/internal/cmd/dns/zone/describe/describe_test.go @@ -7,18 +7,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() @@ -34,7 +33,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, } for _, mod := range mods { mod(flagValues) @@ -57,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiGetZoneRequest)) dns.ApiGetZoneRequest { - request := testClient.GetZone(testCtx, testProjectId, testZoneId) + request := testClient.DefaultAPI.GetZone(testCtx, testProjectId, testZoneId) for _, mod := range mods { mod(&request) } @@ -101,7 +100,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -109,7 +108,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -117,7 +116,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,54 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +161,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -242,11 +194,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.zone); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.zone); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/zone/list/list.go b/internal/cmd/dns/zone/list/list.go index c3de2d640..80f6aa823 100644 --- a/internal/cmd/dns/zone/list/list.go +++ b/internal/cmd/dns/zone/list/list.go @@ -2,13 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" "math" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,17 +18,15 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/dns/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) const ( activeFlag = "active" inactiveFlag = "inactive" nameLikeFlag = "name-like" - orderByNameFlag = "order-by-name" includeDeletedFlag = "include-deleted" limitFlag = "limit" pageSizeFlag = "page-size" @@ -38,6 +36,13 @@ const ( deleteSucceededState = "DELETE_SUCCEEDED" ) +var orderByNameFlag = flags.StringEnumFlag( + "order-by-name", + []string{"asc", "desc"}, + "Order by name,", + flags.StringEnumIgnoreCase[string](), +) + type inputModel struct { *globalflags.GlobalFlagModel @@ -50,7 +55,7 @@ type inputModel struct { PageSize int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists DNS zones", @@ -70,9 +75,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List DNS zones, including deleted`, "$ stackit dns zone list --include-deleted"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -84,21 +89,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Fetch zones - zones, err := fetchZones(ctx, model, apiClient) + zones, err := fetchZones(ctx, model, apiClient.DefaultAPI) if err != nil { return err } - if len(zones) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No zones found for project %q matching the criteria\n", projectLabel) - return nil + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - return outputResult(params.Printer, model.OutputFormat, zones) + return outputResult(params.Printer, model.OutputFormat, projectLabel, zones) }, } configureFlags(cmd) @@ -106,18 +108,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - orderByNameFlagOptions := []string{"asc", "desc"} - cmd.Flags().Bool(activeFlag, false, "Filter for active zones") cmd.Flags().Bool(inactiveFlag, false, "Filter for inactive zones") cmd.Flags().String(nameLikeFlag, "", "Filter by name") - cmd.Flags().Var(flags.EnumFlag(true, "", orderByNameFlagOptions...), orderByNameFlag, fmt.Sprintf("Order by name, one of %q", orderByNameFlagOptions)) + orderByNameFlag.Register(cmd.Flags()) cmd.Flags().Bool(includeDeletedFlag, false, "Includes successfully deleted zones (if unset, these are filtered out)") cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") cmd.Flags().Int64(pageSizeFlag, pageSizeDefault, "Number of items fetched in each API call. Does not affect the number of items in the command output") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -151,20 +151,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Inactive: inactive, IncludeDeleted: flags.FlagToBoolValue(p, cmd, includeDeletedFlag), NameLike: flags.FlagToStringPointer(p, cmd, nameLikeFlag), - OrderByName: flags.FlagToStringPointer(p, cmd, orderByNameFlag), + OrderByName: orderByNameFlag.Ptr(), Limit: limit, PageSize: pageSize, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -180,7 +172,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient dnsClient, p req = req.NameLike(*model.NameLike) } if model.OrderByName != nil { - req = req.OrderByName(strings.ToUpper(*model.OrderByName)) + req = req.OrderByName(dns.ListZonesOrderByNameParameter(strings.ToUpper(*model.OrderByName))) } if !model.IncludeDeleted { req = req.StateNeq(deleteSucceededState) @@ -219,7 +211,7 @@ func fetchZones(ctx context.Context, model *inputModel, apiClient dnsClient) ([] if err != nil { return nil, fmt.Errorf("get DNS zones: %w", err) } - respZones := *resp.Zones + respZones := resp.Zones if len(respZones) == 0 { break } @@ -238,35 +230,23 @@ func fetchZones(ctx context.Context, model *inputModel, apiClient dnsClient) ([] return zones, nil } -func outputResult(p *print.Printer, outputFormat string, zones []dns.Zone) error { - switch outputFormat { - case print.JSONOutputFormat: - // Show details - details, err := json.MarshalIndent(zones, "", " ") - if err != nil { - return fmt.Errorf("marshal DNS zone list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, zones []dns.Zone) error { + return p.OutputResult(outputFormat, zones, func() error { + if len(zones) == 0 { + p.Outputf("No zones found for project %q matching the criteria\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(zones, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal DNS zone list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATE", "TYPE", "DNS NAME", "RECORD COUNT") for i := range zones { z := zones[i] - table.AddRow(utils.PtrString(z.Id), - utils.PtrString(z.Name), - utils.PtrString(z.State), - utils.PtrString(z.Type), - utils.PtrString(z.DnsName), + table.AddRow( + z.Id, + z.Name, + z.State, + z.Type, + z.DnsName, utils.PtrString(z.RecordCount), ) } @@ -276,5 +256,5 @@ func outputResult(p *print.Printer, outputFormat string, zones []dns.Zone) error } return nil - } + }) } diff --git a/internal/cmd/dns/zone/list/list_test.go b/internal/cmd/dns/zone/list/list_test.go index 844c2afa6..a7ac752c9 100644 --- a/internal/cmd/dns/zone/list/list_test.go +++ b/internal/cmd/dns/zone/list/list_test.go @@ -11,27 +11,26 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - nameLikeFlag: "some-pattern", - orderByNameFlag: "asc", + globalflags.ProjectIdFlag: testProjectId, + nameLikeFlag: "some-pattern", + orderByNameFlag.Name(): "asc", } for _, mod := range mods { mod(flagValues) @@ -56,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiListZonesRequest)) dns.ApiListZonesRequest { - request := testClient.ListZones(testCtx, testProjectId) + request := testClient.DefaultAPI.ListZones(testCtx, testProjectId) request = request.NameLike("some-pattern") request = request.OrderByName("ASC") request = request.PageSize(pageSizeDefault) @@ -69,6 +68,7 @@ func fixtureRequest(mods ...func(request *dns.ApiListZonesRequest)) dns.ApiListZ func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -125,7 +125,7 @@ func TestParseInput(t *testing.T) { { description: "required fields only", flagValues: map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, }, isValid: true, expectedModel: &inputModel{ @@ -139,21 +139,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -170,7 +170,7 @@ func TestParseInput(t *testing.T) { { description: "order by name desc", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[orderByNameFlag] = "desc" + flagValues[orderByNameFlag.Name()] = "desc" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -180,14 +180,14 @@ func TestParseInput(t *testing.T) { { description: "order by name invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[orderByNameFlag] = "" + flagValues[orderByNameFlag.Name()] = "" }), isValid: false, }, { description: "order by name invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[orderByNameFlag] = "invalid" + flagValues[orderByNameFlag.Name()] = "invalid" }), isValid: false, }, @@ -209,46 +209,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -306,16 +267,16 @@ func TestBuildRequest(t *testing.T) { PageSize: pageSizeDefault, }, page: 1, - expectedRequest: testClient.ListZones(testCtx, testProjectId).Page(1).PageSize(pageSizeDefault).StateNeq(deleteSucceededState), + expectedRequest: testClient.DefaultAPI.ListZones(testCtx, testProjectId).Page(1).PageSize(pageSizeDefault).StateNeq(deleteSucceededState), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient, tt.page) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI, tt.page) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -453,7 +414,7 @@ func TestFetchZones(t *testing.T) { zones := make([]dns.Zone, numItemsToReturn) mockedResp := dns.ListZonesResponse{ - Zones: &zones, + Zones: zones, } mockedRespBytes, err := json.Marshal(mockedResp) @@ -476,7 +437,7 @@ func TestFetchZones(t *testing.T) { t.Fatalf("Failed to initialize client: %v", err) } - zones, err := fetchZones(testCtx, tt.model, client) + zones, err := fetchZones(testCtx, tt.model, client.DefaultAPI) if err != nil { if !tt.apiCallFails { t.Fatalf("did not fail on invalid input") @@ -499,6 +460,7 @@ func TestFetchZones(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string zones []dns.Zone } tests := []struct { @@ -512,11 +474,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.zones); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.zones); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/dns/zone/update/update.go b/internal/cmd/dns/zone/update/update.go index d7575c609..d700214ff 100644 --- a/internal/cmd/dns/zone/update/update.go +++ b/internal/cmd/dns/zone/update/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api/wait" ) const ( @@ -40,18 +41,18 @@ type inputModel struct { *globalflags.GlobalFlagModel ZoneId string Name *string - DefaultTTL *int64 - Primaries *[]string + DefaultTTL *int32 + Primaries []string Acl *string - RetryTime *int64 - RefreshTime *int64 - NegativeCache *int64 - ExpireTime *int64 + RetryTime *int32 + RefreshTime *int32 + NegativeCache *int32 + ExpireTime *int32 Description *string ContactEmail *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", zoneIdArg), Short: "Updates a DNS zone", @@ -75,18 +76,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient, model.ProjectId, model.ZoneId) + zoneLabel, err := dnsUtils.GetZoneName(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get zone name: %v", err) zoneLabel = model.ZoneId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update zone %s?", zoneLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update zone %s?", zoneLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -101,13 +100,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating zone") - _, err = wait.PartialUpdateZoneWaitHandler(ctx, apiClient, model.ProjectId, model.ZoneId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating zone", func() error { + _, err = wait.PartialUpdateZoneWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.ZoneId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for DNS zone update: %w", err) } - s.Stop() } operationState := "Updated" @@ -124,13 +123,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(nameFlag, "", "User given name of the zone") - cmd.Flags().Int64(defaultTTLFlag, 1000, "Default time to live") + cmd.Flags().Int32(defaultTTLFlag, 1000, "Default time to live") cmd.Flags().StringSlice(primaryFlag, []string{}, "Primary name server for secondary zone") cmd.Flags().String(aclFlag, "", "Access control list") - cmd.Flags().Int64(retryTimeFlag, 0, "Retry time") - cmd.Flags().Int64(refreshTimeFlag, 0, "Refresh time") - cmd.Flags().Int64(negativeCacheFlag, 0, "Negative cache") - cmd.Flags().Int64(expireTimeFlag, 0, "Expire time") + cmd.Flags().Int32(retryTimeFlag, 0, "Retry time") + cmd.Flags().Int32(refreshTimeFlag, 0, "Refresh time") + cmd.Flags().Int32(negativeCacheFlag, 0, "Negative cache") + cmd.Flags().Int32(expireTimeFlag, 0, "Expire time") cmd.Flags().String(descriptionFlag, "", "Description of the zone") cmd.Flags().String(contactEmailFlag, "", "Contact email for the zone") } @@ -144,13 +143,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } name := flags.FlagToStringPointer(p, cmd, nameFlag) - defaultTTL := flags.FlagToInt64Pointer(p, cmd, defaultTTLFlag) - primaries := flags.FlagToStringSlicePointer(p, cmd, primaryFlag) + defaultTTL := flags.FlagToInt32Pointer(p, cmd, defaultTTLFlag) + primaries := flags.FlagToStringSliceValue(p, cmd, primaryFlag) acl := flags.FlagToStringPointer(p, cmd, aclFlag) - retryTime := flags.FlagToInt64Pointer(p, cmd, retryTimeFlag) - refreshTime := flags.FlagToInt64Pointer(p, cmd, refreshTimeFlag) - negativeCache := flags.FlagToInt64Pointer(p, cmd, negativeCacheFlag) - expireTime := flags.FlagToInt64Pointer(p, cmd, expireTimeFlag) + retryTime := flags.FlagToInt32Pointer(p, cmd, retryTimeFlag) + refreshTime := flags.FlagToInt32Pointer(p, cmd, refreshTimeFlag) + negativeCache := flags.FlagToInt32Pointer(p, cmd, negativeCacheFlag) + expireTime := flags.FlagToInt32Pointer(p, cmd, expireTimeFlag) description := flags.FlagToStringPointer(p, cmd, descriptionFlag) contactEmail := flags.FlagToStringPointer(p, cmd, contactEmailFlag) @@ -176,20 +175,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ContactEmail: contactEmail, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *dns.APIClient) dns.ApiPartialUpdateZoneRequest { - req := apiClient.PartialUpdateZone(ctx, model.ProjectId, model.ZoneId) + req := apiClient.DefaultAPI.PartialUpdateZone(ctx, model.ProjectId, model.ZoneId) req = req.PartialUpdateZonePayload(dns.PartialUpdateZonePayload{ Name: model.Name, DefaultTTL: model.DefaultTTL, diff --git a/internal/cmd/dns/zone/update/update_test.go b/internal/cmd/dns/zone/update/update_test.go index adea3637d..0dda139c0 100644 --- a/internal/cmd/dns/zone/update/update_test.go +++ b/internal/cmd/dns/zone/update/update_test.go @@ -4,23 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &dns.APIClient{} +var testClient = &dns.APIClient{DefaultAPI: &dns.DefaultAPIService{}} var testProjectId = uuid.NewString() var testZoneId = uuid.NewString() @@ -36,17 +33,17 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "example", - defaultTTLFlag: "3600", - aclFlag: "0.0.0.0/0", - primaryFlag: "1.1.1.1", - retryTimeFlag: "600", - refreshTimeFlag: "3600", - negativeCacheFlag: "60", - expireTimeFlag: "36000000", - descriptionFlag: "Example", - contactEmailFlag: "example@example.com", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "example", + defaultTTLFlag: "3600", + aclFlag: "0.0.0.0/0", + primaryFlag: "1.1.1.1", + retryTimeFlag: "600", + refreshTimeFlag: "3600", + negativeCacheFlag: "60", + expireTimeFlag: "36000000", + descriptionFlag: "Example", + contactEmailFlag: "example@example.com", } for _, mod := range mods { mod(flagValues) @@ -62,13 +59,13 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { }, ZoneId: testZoneId, Name: utils.Ptr("example"), - DefaultTTL: utils.Ptr(int64(3600)), - Primaries: utils.Ptr([]string{"1.1.1.1"}), + DefaultTTL: utils.Ptr(int32(3600)), + Primaries: []string{"1.1.1.1"}, Acl: utils.Ptr("0.0.0.0/0"), - RetryTime: utils.Ptr(int64(600)), - RefreshTime: utils.Ptr(int64(3600)), - NegativeCache: utils.Ptr(int64(60)), - ExpireTime: utils.Ptr(int64(36000000)), + RetryTime: utils.Ptr(int32(600)), + RefreshTime: utils.Ptr(int32(3600)), + NegativeCache: utils.Ptr(int32(60)), + ExpireTime: utils.Ptr(int32(36000000)), Description: utils.Ptr("Example"), ContactEmail: utils.Ptr("example@example.com"), } @@ -79,16 +76,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *dns.ApiPartialUpdateZoneRequest)) dns.ApiPartialUpdateZoneRequest { - request := testClient.PartialUpdateZone(testCtx, testProjectId, testZoneId) + request := testClient.DefaultAPI.PartialUpdateZone(testCtx, testProjectId, testZoneId) request = request.PartialUpdateZonePayload(dns.PartialUpdateZonePayload{ Name: utils.Ptr("example"), - DefaultTTL: utils.Ptr(int64(3600)), - Primaries: utils.Ptr([]string{"1.1.1.1"}), + DefaultTTL: utils.Ptr(int32(3600)), + Primaries: []string{"1.1.1.1"}, Acl: utils.Ptr("0.0.0.0/0"), - RetryTime: utils.Ptr(int64(600)), - RefreshTime: utils.Ptr(int64(3600)), - NegativeCache: utils.Ptr(int64(60)), - ExpireTime: utils.Ptr(int64(36000000)), + RetryTime: utils.Ptr(int32(600)), + RefreshTime: utils.Ptr(int32(3600)), + NegativeCache: utils.Ptr(int32(60)), + ExpireTime: utils.Ptr(int32(36000000)), Description: utils.Ptr("Example"), ContactEmail: utils.Ptr("example@example.com"), }) @@ -136,7 +133,7 @@ func TestParseInput(t *testing.T) { description: "required flags only (no values to update)", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, }, isValid: false, expectedModel: &inputModel{ @@ -151,17 +148,17 @@ func TestParseInput(t *testing.T) { description: "zero values", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "", - defaultTTLFlag: "0", - aclFlag: "", - primaryFlag: "", - retryTimeFlag: "0", - refreshTimeFlag: "0", - negativeCacheFlag: "0", - expireTimeFlag: "0", - descriptionFlag: "", - contactEmailFlag: "", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "", + defaultTTLFlag: "0", + aclFlag: "", + primaryFlag: "", + retryTimeFlag: "0", + refreshTimeFlag: "0", + negativeCacheFlag: "0", + expireTimeFlag: "0", + descriptionFlag: "", + contactEmailFlag: "", }, isValid: true, expectedModel: &inputModel{ @@ -171,13 +168,13 @@ func TestParseInput(t *testing.T) { }, ZoneId: testZoneId, Name: utils.Ptr(""), - DefaultTTL: utils.Ptr(int64(0)), - Primaries: utils.Ptr([]string{}), + DefaultTTL: utils.Ptr(int32(0)), + Primaries: []string{}, Acl: utils.Ptr(""), - RetryTime: utils.Ptr(int64(0)), - RefreshTime: utils.Ptr(int64(0)), - NegativeCache: utils.Ptr(int64(0)), - ExpireTime: utils.Ptr(int64(0)), + RetryTime: utils.Ptr(int32(0)), + RefreshTime: utils.Ptr(int32(0)), + NegativeCache: utils.Ptr(int32(0)), + ExpireTime: utils.Ptr(int32(0)), Description: utils.Ptr(""), ContactEmail: utils.Ptr(""), }, @@ -186,7 +183,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -194,7 +191,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -202,7 +199,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -225,9 +222,8 @@ func TestParseInput(t *testing.T) { primaryFlagValues: []string{"1.2.3.4", "5.6.7.8"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Primaries = utils.Ptr( - append(*model.Primaries, "1.2.3.4", "5.6.7.8"), - ) + model.Primaries = + append(model.Primaries, "1.2.3.4", "5.6.7.8") }), }, { @@ -237,17 +233,16 @@ func TestParseInput(t *testing.T) { primaryFlagValues: []string{"1.2.3.4,5.6.7.8"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Primaries = utils.Ptr( - append(*model.Primaries, "1.2.3.4", "5.6.7.8"), - ) + model.Primaries = + append(model.Primaries, "1.2.3.4", "5.6.7.8") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -289,7 +284,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -328,7 +323,7 @@ func TestBuildRequest(t *testing.T) { }, ZoneId: testZoneId, }, - expectedRequest: testClient.PartialUpdateZone(testCtx, testProjectId, testZoneId). + expectedRequest: testClient.DefaultAPI.PartialUpdateZone(testCtx, testProjectId, testZoneId). PartialUpdateZonePayload(dns.PartialUpdateZonePayload{}), }, } @@ -338,7 +333,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, dns.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/dns/zone/zone.go b/internal/cmd/dns/zone/zone.go index b81a143ff..ecfb1240a 100644 --- a/internal/cmd/dns/zone/zone.go +++ b/internal/cmd/dns/zone/zone.go @@ -7,14 +7,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone/list" "github.com/stackitcloud/stackit-cli/internal/cmd/dns/zone/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "zone", Short: "Provides functionality for DNS zones", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/git/flavor/flavor.go b/internal/cmd/git/flavor/flavor.go index c6f4c512f..ee1700900 100644 --- a/internal/cmd/git/flavor/flavor.go +++ b/internal/cmd/git/flavor/flavor.go @@ -2,14 +2,14 @@ package flavor import ( "github.com/stackitcloud/stackit-cli/internal/cmd/git/flavor/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "flavor", Short: "Provides functionality for STACKIT Git flavors", @@ -21,7 +21,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( list.NewCmd(params), ) diff --git a/internal/cmd/git/flavor/list/list.go b/internal/cmd/git/flavor/list/list.go index aec140d80..9fd74f475 100644 --- a/internal/cmd/git/flavor/list/list.go +++ b/internal/cmd/git/flavor/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/git/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type inputModel struct { @@ -28,7 +27,7 @@ type inputModel struct { const limitFlag = "limit" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists instances flavors of STACKIT Git.", @@ -43,15 +42,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit git flavor list --limit=10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } // Configure API client - apiClient, err := client.ConfigureClient(params.Printer) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -62,19 +61,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get STACKIT Git flavors: %w", err) } - flavors := *resp.Flavors - if len(flavors) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No flavors found for project %q\n", projectLabel) - return nil - } else if model.Limit != nil && len(flavors) > int(*model.Limit) { + flavors := resp.GetFlavors() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + // Truncate output + if model.Limit != nil && len(flavors) > int(*model.Limit) { flavors = (flavors)[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, flavors) + + return outputResult(params.Printer, model.OutputFormat, projectLabel, flavors) }, } configureFlags(cmd) @@ -85,7 +85,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -104,51 +104,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *git.APIClient) git.ApiListFlavorsRequest { - return apiClient.ListFlavors(ctx, model.ProjectId) + return apiClient.DefaultAPI.ListFlavors(ctx, model.ProjectId) } -func outputResult(p *print.Printer, outputFormat string, flavors []git.Flavor) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(flavors, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability flavor list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, flavors []git.Flavor) error { + return p.OutputResult(outputFormat, flavors, func() error { + if len(flavors) == 0 { + p.Outputf("No flavors found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(flavors, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability flavor list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "DESCRIPTION", "DISPLAY_NAME", "AVAILABLE", "SKU") for i := range flavors { flavor := (flavors)[i] table.AddRow( - utils.PtrString(flavor.Id), - utils.PtrString(flavor.Description), - utils.PtrString(flavor.DisplayName), - utils.PtrString(flavor.Availability), - utils.PtrString(flavor.Sku), + flavor.Id, + flavor.Description, + flavor.DisplayName, + flavor.Availability, + flavor.Sku, ) } err := table.Display(p) @@ -157,5 +137,5 @@ func outputResult(p *print.Printer, outputFormat string, flavors []git.Flavor) e } return nil - } + }) } diff --git a/internal/cmd/git/flavor/list/list_test.go b/internal/cmd/git/flavor/list/list_test.go index 1413630b0..904b0c7d3 100644 --- a/internal/cmd/git/flavor/list/list_test.go +++ b/internal/cmd/git/flavor/list/list_test.go @@ -8,17 +8,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &git.APIClient{} +var testClient = &git.APIClient{DefaultAPI: &git.DefaultAPIService{}} var testProjectId = uuid.NewString() const ( @@ -49,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *git.ApiListFlavorsRequest)) git.ApiListFlavorsRequest { - request := testClient.ListFlavors(testCtx, testProjectId) + request := testClient.DefaultAPI.ListFlavors(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -59,6 +60,7 @@ func fixtureRequest(mods ...func(request *git.ApiListFlavorsRequest)) git.ApiLis func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -123,46 +125,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -185,7 +148,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, git.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -198,6 +161,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string flavors []git.Flavor } tests := []struct { @@ -225,11 +189,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.flavors); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.flavors); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/git/git.go b/internal/cmd/git/git.go index 72605d968..624702686 100644 --- a/internal/cmd/git/git.go +++ b/internal/cmd/git/git.go @@ -3,14 +3,14 @@ package git import ( "github.com/stackitcloud/stackit-cli/internal/cmd/git/flavor" "github.com/stackitcloud/stackit-cli/internal/cmd/git/instance" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "git", Short: "Provides functionality for STACKIT Git", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( instance.NewCmd(params), flavor.NewCmd(params), diff --git a/internal/cmd/git/instance/create/create.go b/internal/cmd/git/instance/create/create.go index 761c2be61..6c4c046e4 100644 --- a/internal/cmd/git/instance/create/create.go +++ b/internal/cmd/git/instance/create/create.go @@ -2,12 +2,15 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,9 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/git/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/stackit-sdk-go/services/git/wait" ) const ( @@ -29,13 +29,12 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - Id *string Name string Flavor string Acl []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates STACKIT Git instance", @@ -48,32 +47,30 @@ func NewCmd(params *params.CmdParams) *cobra.Command { ), examples.NewExample( `Create a instance with name 'my-new-instance' and flavor`, - `$ stackit git instance create --name my-new-instance --flavor git-100'`, + `$ stackit git instance create --name my-new-instance --flavor git-100`, ), examples.NewExample( `Create a instance with name 'my-new-instance' and acl`, - `$ stackit git instance create --name my-new-instance --acl 1.1.1.1/1'`, + `$ stackit git instance create --name my-new-instance --acl 1.1.1.1/1`, ), ), - RunE: func(cmd *cobra.Command, _ []string) (err error) { + RunE: func(cmd *cobra.Command, args []string) (err error) { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } // Configure API client - apiClient, err := client.ConfigureClient(params.Printer) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create the instance %q?", model.Name) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create the instance %q?", model.Name) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -83,20 +80,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create stackit git instance: %w", err) } - model.Id = result.Id // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating stackit git instance") - _, err = wait.CreateGitInstanceWaitHandler(ctx, apiClient, model.ProjectId, *model.Id).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating STACKIT Git instance", func() error { + _, err = wait.CreateGitInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, result.Id).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for stackit git Instance creation: %w", err) } - s.Stop() } - return outputResult(params.Printer, model, result) + return outputResult(params.Printer, model.OutputFormat, model.Async, model.Name, result) }, } @@ -113,7 +109,7 @@ func configureFlags(cmd *cobra.Command) { } } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { @@ -130,57 +126,33 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Acl: acl, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *git.APIClient) git.ApiCreateInstanceRequest { - return apiClient.CreateInstance(ctx, model.ProjectId).CreateInstancePayload(createPayload(model)) + return apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId).CreateInstancePayload(createPayload(model)) } func createPayload(model *inputModel) git.CreateInstancePayload { return git.CreateInstancePayload{ - Name: &model.Name, - Flavor: git.CreateInstancePayloadGetFlavorAttributeType(&model.Flavor), - Acl: &model.Acl, + Name: model.Name, + Flavor: utils.Ptr(git.CreateInstancePayloadFlavor(model.Flavor)), + Acl: model.Acl, } } -func outputResult(p *print.Printer, model *inputModel, resp *git.Instance) error { - if model == nil { - return fmt.Errorf("input model is nil") +func outputResult(p *print.Printer, outputFormat string, async bool, instanceName string, resp *git.Instance) error { + if resp == nil { + return fmt.Errorf("API resp is nil") } - var outputFormat string - if model.GlobalFlagModel != nil { - outputFormat = model.OutputFormat - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal instance: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal iminstanceage: %w", err) + return p.OutputResult(outputFormat, resp, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" } - p.Outputln(string(details)) - + p.Outputf("%s instance %q with id %s\n", operationState, instanceName, resp.Id) return nil - default: - p.Outputf("Created instance %q with id %s\n", model.Name, utils.PtrString(model.Id)) - return nil - } + }) } diff --git a/internal/cmd/git/instance/create/create_test.go b/internal/cmd/git/instance/create/create_test.go index 5b46e1eef..212331500 100644 --- a/internal/cmd/git/instance/create/create_test.go +++ b/internal/cmd/git/instance/create/create_test.go @@ -7,18 +7,20 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &git.APIClient{} + testClient = &git.APIClient{DefaultAPI: &git.DefaultAPIService{}} testProjectId = uuid.NewString() testName = "test-instance" @@ -55,9 +57,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureCreatePayload(mods ...func(payload *git.CreateInstancePayload)) (payload git.CreateInstancePayload) { payload = git.CreateInstancePayload{ - Name: &testName, - Flavor: git.CreateInstancePayloadGetFlavorAttributeType(&testFlavor), - Acl: &testAcl, + Name: testName, + Flavor: utils.Ptr(git.CreateInstancePayloadFlavor(testFlavor)), + Acl: testAcl, } for _, mod := range mods { mod(&payload) @@ -66,7 +68,7 @@ func fixtureCreatePayload(mods ...func(payload *git.CreateInstancePayload)) (pay } func fixtureRequest(mods ...func(request *git.ApiCreateInstanceRequest)) git.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId) request = request.CreateInstancePayload(fixtureCreatePayload()) @@ -79,6 +81,7 @@ func fixtureRequest(mods ...func(request *git.ApiCreateInstanceRequest)) git.Api func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -126,51 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateFlagGroups(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flag groups: %v", err) - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -192,8 +151,8 @@ func TestBuildRequest(t *testing.T) { model.Name = "new-name" }), expectedRequest: fixtureRequest(func(request *git.ApiCreateInstanceRequest) { - *request = (*request).CreateInstancePayload(fixtureCreatePayload(func(payload *git.CreateInstancePayload) { - payload.Name = utils.Ptr("new-name") + *request = request.CreateInstancePayload(fixtureCreatePayload(func(payload *git.CreateInstancePayload) { + payload.Name = "new-name" })) }), }, @@ -205,7 +164,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), - cmp.AllowUnexported(git.NullableString{}), + cmp.AllowUnexported(git.NullableString{}, git.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -216,8 +175,10 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - model *inputModel - resp *git.Instance + outputFormat string + async bool + instanceName string + resp *git.Instance } tests := []struct { name string @@ -225,39 +186,41 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "nil", + name: "nil response", args: args{ - model: nil, - resp: nil, + outputFormat: "", + async: false, + instanceName: "", + resp: nil, }, wantErr: true, }, { name: "empty input", args: args{ - model: &inputModel{}, - resp: &git.Instance{}, + outputFormat: "", + async: false, + instanceName: "", + resp: &git.Instance{Id: uuid.NewString()}, }, wantErr: false, }, { name: "output json", args: args{ - model: &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ - OutputFormat: print.JSONOutputFormat, - }, - }, - resp: nil, + outputFormat: print.JSONOutputFormat, + async: true, + instanceName: testName, + resp: &git.Instance{Id: uuid.NewString()}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.instanceName, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/git/instance/delete/delete.go b/internal/cmd/git/instance/delete/delete.go index c16a6b09e..ed8e4e4d5 100644 --- a/internal/cmd/git/instance/delete/delete.go +++ b/internal/cmd/git/instance/delete/delete.go @@ -4,9 +4,12 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +20,6 @@ import ( gitUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/git/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" - "github.com/stackitcloud/stackit-sdk-go/services/git/wait" ) type inputModel struct { @@ -28,7 +29,7 @@ type inputModel struct { const instanceIdArg = "INSTANCE_ID" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes STACKIT Git instance", @@ -46,7 +47,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureClient(params.Printer) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -57,7 +58,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectName = model.ProjectId } - instanceName, err := gitUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceName, err := gitUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get stackit git intance name: %v", err) instanceName = model.InstanceId @@ -65,12 +66,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { instanceName = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the stackit git instance %q for %q?", instanceName, projectName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the stackit git instance %q for %q?", instanceName, projectName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -83,13 +82,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting stackit git instance") - _, err = wait.DeleteGitInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting STACKIT Git instance", func() error { + _, err = wait.DeleteGitInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for stackit git instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -116,18 +115,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM InstanceId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *git.APIClient) git.ApiDeleteInstanceRequest { - return apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + return apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.InstanceId) } diff --git a/internal/cmd/git/instance/delete/delete_test.go b/internal/cmd/git/instance/delete/delete_test.go index 8c90a4f1d..f3a817e2f 100644 --- a/internal/cmd/git/instance/delete/delete_test.go +++ b/internal/cmd/git/instance/delete/delete_test.go @@ -4,21 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/git" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &git.APIClient{} + testClient = &git.APIClient{DefaultAPI: &git.DefaultAPIService{}} testProjectId = uuid.NewString() testInstanceId = uuid.NewString() ) @@ -45,7 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *git.ApiDeleteInstanceRequest)) git.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -55,6 +55,7 @@ func fixtureRequest(mods ...func(request *git.ApiDeleteInstanceRequest)) git.Api func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string args []string isValid bool @@ -103,8 +104,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -135,7 +136,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -171,7 +172,7 @@ func TestBuildRequest(t *testing.T) { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, git.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/git/instance/describe/describe.go b/internal/cmd/git/instance/describe/describe.go index 8e2f266e1..732148e91 100644 --- a/internal/cmd/git/instance/describe/describe.go +++ b/internal/cmd/git/instance/describe/describe.go @@ -2,13 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/git/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type inputModel struct { @@ -27,7 +26,7 @@ type inputModel struct { const instanceIdArg = "INSTANCE_ID" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Describes STACKIT Git instance", @@ -44,7 +43,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Configure API client - apiClient, err := client.ConfigureClient(params.Printer) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -79,74 +78,39 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM InstanceId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *git.APIClient) git.ApiGetInstanceRequest { - return apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + return apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId) } func outputResult(p *print.Printer, outputFormat string, resp *git.Instance) error { if resp == nil { return fmt.Errorf("instance not found") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal instance: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { table := tables.NewTable() - if id := resp.Id; id != nil { - table.AddRow("ID", *id) - table.AddSeparator() - } - if name := resp.Name; name != nil { - table.AddRow("NAME", *name) - table.AddSeparator() - } - if url := resp.Url; url != nil { - table.AddRow("URL", *url) - table.AddSeparator() - } - if version := resp.Version; version != nil { - table.AddRow("VERSION", *version) - table.AddSeparator() - } - if state := resp.State; state != nil { - table.AddRow("STATE", *state) - table.AddSeparator() - } - if created := resp.Created; created != nil { - table.AddRow("CREATED", *created) - table.AddSeparator() - } + + table.AddRow("ID", resp.Id) + table.AddSeparator() + table.AddRow("NAME", resp.Name) + table.AddSeparator() + table.AddRow("URL", resp.Url) + table.AddSeparator() + table.AddRow("VERSION", resp.Version) + table.AddSeparator() + table.AddRow("STATE", resp.State) + table.AddSeparator() + table.AddRow("CREATED", resp.Created) + table.AddSeparator() if err := table.Display(p); err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/git/instance/describe/describe_test.go b/internal/cmd/git/instance/describe/describe_test.go index 17cd8dae8..f2589c578 100644 --- a/internal/cmd/git/instance/describe/describe_test.go +++ b/internal/cmd/git/instance/describe/describe_test.go @@ -7,17 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &git.APIClient{} + testClient = &git.APIClient{DefaultAPI: &git.DefaultAPIService{}} testProjectId = uuid.NewString() testInstanceId = []string{uuid.NewString()} ) @@ -44,7 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *git.ApiGetInstanceRequest)) git.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId[0]) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId[0]) for _, mod := range mods { mod(&request) } @@ -54,6 +55,7 @@ func fixtureRequest(mods ...func(request *git.ApiGetInstanceRequest)) git.ApiGet func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool args []string @@ -117,8 +119,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) if err := globalflags.Configure(cmd.Flags()); err != nil { t.Errorf("cannot configure global flags: %v", err) } @@ -145,7 +147,7 @@ func TestParseInput(t *testing.T) { } } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -181,7 +183,7 @@ func TestBuildRequest(t *testing.T) { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, git.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -214,11 +216,11 @@ func TestOutputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/git/instance/instance.go b/internal/cmd/git/instance/instance.go index b2d66b43a..bd36a1cbd 100644 --- a/internal/cmd/git/instance/instance.go +++ b/internal/cmd/git/instance/instance.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/git/instance/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/git/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/git/instance/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for STACKIT Git instances", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( list.NewCmd(params), describe.NewCmd(params), diff --git a/internal/cmd/git/instance/list/list.go b/internal/cmd/git/instance/list/list.go index 0057342ff..65305becd 100644 --- a/internal/cmd/git/instance/list/list.go +++ b/internal/cmd/git/instance/list/list.go @@ -2,13 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/git/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type inputModel struct { @@ -29,10 +27,10 @@ type inputModel struct { const limitFlag = "limit" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", - Short: "Lists all instances of STACKIT Git.", + Short: "Lists all instances of STACKIT Git", Long: "Lists all instances of STACKIT Git for the current project.", Args: args.NoArgs, Example: examples.Build( @@ -44,15 +42,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit git instance list --limit=10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } // Configure API client - apiClient, err := client.ConfigureClient(params.Printer) + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } @@ -63,19 +61,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get STACKIT Git instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil - } else if model.Limit != nil && len(instances) > int(*model.Limit) { + instances := resp.GetInstances() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + // Truncate output + if model.Limit != nil && len(instances) > int(*model.Limit) { instances = (instances)[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } configureFlags(cmd) @@ -86,7 +85,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -105,52 +104,32 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *git.APIClient) git.ApiListInstancesRequest { - return apiClient.ListInstances(ctx, model.ProjectId) + return apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId) } -func outputResult(p *print.Printer, outputFormat string, instances []git.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []git.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "URL", "VERSION", "STATE", "CREATED") for i := range instances { instance := (instances)[i] table.AddRow( - utils.PtrString(instance.Id), - utils.PtrString(instance.Name), - utils.PtrString(instance.Url), - utils.PtrString(instance.Version), - utils.PtrString(instance.State), - utils.PtrString(instance.Created), + instance.Id, + instance.Name, + instance.Url, + instance.Version, + instance.State, + instance.Created, ) } err := table.Display(p) @@ -159,5 +138,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []git.Instanc } return nil - } + }) } diff --git a/internal/cmd/git/instance/list/list_test.go b/internal/cmd/git/instance/list/list_test.go index f73297388..1d77b55b4 100644 --- a/internal/cmd/git/instance/list/list_test.go +++ b/internal/cmd/git/instance/list/list_test.go @@ -8,17 +8,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &git.APIClient{} +var testClient = &git.APIClient{DefaultAPI: &git.DefaultAPIService{}} var testProjectId = uuid.NewString() const ( @@ -49,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *git.ApiListInstancesRequest)) git.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -59,6 +60,7 @@ func fixtureRequest(mods ...func(request *git.ApiListInstancesRequest)) git.ApiL func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -123,46 +125,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -185,7 +148,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, git.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -198,6 +161,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []git.Instance } tests := []struct { @@ -225,11 +189,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/image/create/create.go b/internal/cmd/image/create/create.go index 3ffe26a68..09568f247 100644 --- a/internal/cmd/image/create/create.go +++ b/internal/cmd/image/create/create.go @@ -3,7 +3,6 @@ package create import ( "bufio" "context" - "encoding/json" goerrors "errors" "fmt" "io" @@ -11,9 +10,11 @@ import ( "os" "time" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,7 +23,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -72,11 +72,11 @@ type imageConfig struct { type inputModel struct { *globalflags.GlobalFlagModel - Id *string + Id string Name string DiskFormat string LocalFilePath string - Labels *map[string]string + Labels map[string]any Config *imageConfig MinDiskSize *int64 MinRam *int64 @@ -84,7 +84,7 @@ type inputModel struct { NoProgressIndicator *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates images", @@ -104,9 +104,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit image create --name my-new-image --disk-format=raw --local-file-path=/my/raw/image --uefi=false`, ), ), - RunE: func(cmd *cobra.Command, _ []string) (err error) { + RunE: func(cmd *cobra.Command, args []string) (err error) { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -128,12 +128,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } }() - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create the image %q?", model.Name) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create the image %q?", model.Name) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -148,7 +146,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if !ok { return fmt.Errorf("create image: no upload URL has been provided") } - if err := uploadAsync(ctx, params.Printer, model, file, url); err != nil { + if err := uploadAsync(ctx, params.Printer, model, file, *url); err != nil { return err } @@ -294,7 +292,7 @@ func configureFlags(cmd *cobra.Command) { cmd.MarkFlagsRequiredTogether(rescueBusFlag, rescueDeviceFlag) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -306,7 +304,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Name: name, DiskFormat: flags.FlagToStringValue(p, cmd, diskFormatFlag), LocalFilePath: flags.FlagToStringValue(p, cmd, localFilePathFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelsFlag), NoProgressIndicator: flags.FlagToBoolPointer(p, cmd, noProgressIndicatorFlag), Config: &imageConfig{ Architecture: flags.FlagToStringPointer(p, cmd, architectureFlag), @@ -329,29 +327,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Protected: flags.FlagToBoolPointer(p, cmd, protectedFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateImageRequest { - request := apiClient.CreateImage(ctx, model.ProjectId). + request := apiClient.DefaultAPI.CreateImage(ctx, model.ProjectId, model.Region). CreateImagePayload(createPayload(ctx, model)) return request } func createPayload(_ context.Context, model *inputModel) iaas.CreateImagePayload { payload := iaas.CreateImagePayload{ - DiskFormat: &model.DiskFormat, - Name: &model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + DiskFormat: model.DiskFormat, + Name: model.Name, + Labels: model.Labels, MinDiskSize: model.MinDiskSize, MinRam: model.MinRam, Protected: model.Protected, @@ -366,34 +356,34 @@ func createPayload(_ context.Context, model *inputModel) iaas.CreateImagePayload payload.Config.BootMenu = model.Config.BootMenu } if config.CdromBus != nil { - payload.Config.CdromBus = iaas.NewNullableString(model.Config.CdromBus) + payload.Config.CdromBus = *iaas.NewNullableString(model.Config.CdromBus) } if config.DiskBus != nil { - payload.Config.DiskBus = iaas.NewNullableString(config.DiskBus) + payload.Config.DiskBus = *iaas.NewNullableString(config.DiskBus) } if config.NicModel != nil { - payload.Config.NicModel = iaas.NewNullableString(config.NicModel) + payload.Config.NicModel = *iaas.NewNullableString(config.NicModel) } if config.OperatingSystem != nil { payload.Config.OperatingSystem = config.OperatingSystem } if config.OperatingSystemDistro != nil { - payload.Config.OperatingSystemDistro = iaas.NewNullableString(config.OperatingSystemDistro) + payload.Config.OperatingSystemDistro = *iaas.NewNullableString(config.OperatingSystemDistro) } if config.OperatingSystemVersion != nil { - payload.Config.OperatingSystemVersion = iaas.NewNullableString(config.OperatingSystemVersion) + payload.Config.OperatingSystemVersion = *iaas.NewNullableString(config.OperatingSystemVersion) } if config.RescueBus != nil { - payload.Config.RescueBus = iaas.NewNullableString(config.RescueBus) + payload.Config.RescueBus = *iaas.NewNullableString(config.RescueBus) } if config.RescueDevice != nil { - payload.Config.RescueDevice = iaas.NewNullableString(config.RescueDevice) + payload.Config.RescueDevice = *iaas.NewNullableString(config.RescueDevice) } if config.SecureBoot != nil { payload.Config.SecureBoot = config.SecureBoot } if config.VideoModel != nil { - payload.Config.VideoModel = iaas.NewNullableString(config.VideoModel) + payload.Config.VideoModel = *iaas.NewNullableString(config.VideoModel) } if config.VirtioScsi != nil { payload.Config.VirtioScsi = config.VirtioScsi @@ -411,25 +401,9 @@ func outputResult(p *print.Printer, model *inputModel, resp *iaas.ImageCreateRes if model.GlobalFlagModel != nil { outputFormat = model.OutputFormat } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal image: %w", err) - } - p.Outputln(string(details)) + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created image %q with id %s\n", model.Name, model.Id) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal image: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created image %q with id %s\n", model.Name, utils.PtrString(model.Id)) - return nil - } + }) } diff --git a/internal/cmd/image/create/create_test.go b/internal/cmd/image/create/create_test.go index 45a926fbf..2e4ba2ee8 100644 --- a/internal/cmd/image/create/create_test.go +++ b/internal/cmd/image/create/create_test.go @@ -9,22 +9,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag - -type testCtxKey struct{} - -var ( - testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} - testProjectId = uuid.NewString() - +const ( + testRegion = "eu01" testLocalImagePath = "/does/not/exist" testDiskFormat = "raw" testDiskSize int64 = 16 * 1024 * 1024 * 1024 @@ -48,9 +43,18 @@ var ( testLabels = "foo=FOO,bar=BAR,baz=BAZ" ) +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, nameFlag: testName, diskFormatFlag: testDiskFormat, @@ -80,8 +84,8 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st return flagValues } -func parseLabels(labelstring string) map[string]string { - labels := map[string]string{} +func parseLabels(labelstring string) map[string]any { + labels := map[string]any{} for _, part := range strings.Split(labelstring, ",") { v := strings.Split(part, "=") labels[v[0]] = v[1] @@ -92,30 +96,34 @@ func parseLabels(labelstring string) map[string]string { func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - Name: testName, - DiskFormat: testDiskFormat, - LocalFilePath: testLocalImagePath, - Labels: utils.Ptr(parseLabels(testLabels)), + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: testName, + DiskFormat: testDiskFormat, + LocalFilePath: testLocalImagePath, + Labels: parseLabels(testLabels), Config: &imageConfig{ - Architecture: &testArchitecture, - BootMenu: &testBootmenu, - CdromBus: &testCdRomBus, - DiskBus: &testDiskBus, - NicModel: &testNicModel, - OperatingSystem: &testOperatingSystem, - OperatingSystemDistro: &testOperatingSystemDistro, - OperatingSystemVersion: &testOperatingSystemVersion, - RescueBus: &testRescueBus, - RescueDevice: &testRescueDevice, - SecureBoot: &testSecureBoot, + Architecture: utils.Ptr(testArchitecture), + BootMenu: utils.Ptr(testBootmenu), + CdromBus: utils.Ptr(testCdRomBus), + DiskBus: utils.Ptr(testDiskBus), + NicModel: utils.Ptr(testNicModel), + OperatingSystem: utils.Ptr(testOperatingSystem), + OperatingSystemDistro: utils.Ptr(testOperatingSystemDistro), + OperatingSystemVersion: utils.Ptr(testOperatingSystemVersion), + RescueBus: utils.Ptr(testRescueBus), + RescueDevice: utils.Ptr(testRescueDevice), + SecureBoot: utils.Ptr(testSecureBoot), Uefi: testUefi, - VideoModel: &testVideoModel, - VirtioScsi: &testVirtioScsi, + VideoModel: utils.Ptr(testVideoModel), + VirtioScsi: utils.Ptr(testVirtioScsi), }, - MinDiskSize: &testDiskSize, - MinRam: &testRamSize, - Protected: &testProtected, + MinDiskSize: utils.Ptr(testDiskSize), + MinRam: utils.Ptr(testRamSize), + Protected: utils.Ptr(testProtected), } for _, mod := range mods { mod(model) @@ -126,31 +134,31 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureCreatePayload(mods ...func(payload *iaas.CreateImagePayload)) (payload iaas.CreateImagePayload) { payload = iaas.CreateImagePayload{ Config: &iaas.ImageConfig{ - Architecture: &testArchitecture, - BootMenu: &testBootmenu, - CdromBus: iaas.NewNullableString(&testCdRomBus), - DiskBus: iaas.NewNullableString(&testDiskBus), - NicModel: iaas.NewNullableString(&testNicModel), - OperatingSystem: &testOperatingSystem, - OperatingSystemDistro: iaas.NewNullableString(&testOperatingSystemDistro), - OperatingSystemVersion: iaas.NewNullableString(&testOperatingSystemVersion), - RescueBus: iaas.NewNullableString(&testRescueBus), - RescueDevice: iaas.NewNullableString(&testRescueDevice), - SecureBoot: &testSecureBoot, - Uefi: &testUefi, - VideoModel: iaas.NewNullableString(&testVideoModel), - VirtioScsi: &testVirtioScsi, + Architecture: utils.Ptr(testArchitecture), + BootMenu: utils.Ptr(testBootmenu), + CdromBus: *iaas.NewNullableString(utils.Ptr(testCdRomBus)), + DiskBus: *iaas.NewNullableString(utils.Ptr(testDiskBus)), + NicModel: *iaas.NewNullableString(utils.Ptr(testNicModel)), + OperatingSystem: utils.Ptr(testOperatingSystem), + OperatingSystemDistro: *iaas.NewNullableString(utils.Ptr(testOperatingSystemDistro)), + OperatingSystemVersion: *iaas.NewNullableString(utils.Ptr(testOperatingSystemVersion)), + RescueBus: *iaas.NewNullableString(utils.Ptr(testRescueBus)), + RescueDevice: *iaas.NewNullableString(utils.Ptr(testRescueDevice)), + SecureBoot: utils.Ptr(testSecureBoot), + Uefi: utils.Ptr(testUefi), + VideoModel: *iaas.NewNullableString(utils.Ptr(testVideoModel)), + VirtioScsi: utils.Ptr(testVirtioScsi), }, - DiskFormat: &testDiskFormat, - Labels: &map[string]interface{}{ + DiskFormat: testDiskFormat, + Labels: map[string]any{ "foo": "FOO", "bar": "BAR", "baz": "BAZ", }, - MinDiskSize: &testDiskSize, - MinRam: &testRamSize, - Name: &testName, - Protected: &testProtected, + MinDiskSize: utils.Ptr(testDiskSize), + MinRam: utils.Ptr(testRamSize), + Name: testName, + Protected: utils.Ptr(testProtected), } for _, mod := range mods { mod(&payload) @@ -159,7 +167,7 @@ func fixtureCreatePayload(mods ...func(payload *iaas.CreateImagePayload)) (paylo } func fixtureRequest(mods ...func(request *iaas.ApiCreateImageRequest)) iaas.ApiCreateImageRequest { - request := testClient.CreateImage(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateImage(testCtx, testProjectId, testRegion) request = request.CreateImagePayload(fixtureCreatePayload()) @@ -172,6 +180,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateImageRequest)) iaas.ApiC func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -190,21 +199,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -232,7 +241,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ + model.Labels = map[string]any{ "foo": "bar", } }), @@ -277,51 +286,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateFlagGroups(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flag groups: %v", err) - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -343,7 +308,7 @@ func TestBuildRequest(t *testing.T) { model.Labels = nil }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateImageRequest) { - *request = (*request).CreateImagePayload(fixtureCreatePayload(func(payload *iaas.CreateImagePayload) { + *request = request.CreateImagePayload(fixtureCreatePayload(func(payload *iaas.CreateImagePayload) { payload.Labels = nil })) }), @@ -354,8 +319,8 @@ func TestBuildRequest(t *testing.T) { model.Config.CdromBus = utils.Ptr("foobar") }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateImageRequest) { - *request = (*request).CreateImagePayload(fixtureCreatePayload(func(payload *iaas.CreateImagePayload) { - payload.Config.CdromBus = iaas.NewNullableString(utils.Ptr("foobar")) + *request = request.CreateImagePayload(fixtureCreatePayload(func(payload *iaas.CreateImagePayload) { + payload.Config.CdromBus = *iaas.NewNullableString(utils.Ptr("foobar")) })) }), }, @@ -365,7 +330,7 @@ func TestBuildRequest(t *testing.T) { model.Config.Uefi = false }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateImageRequest) { - *request = (*request).CreateImagePayload(fixtureCreatePayload(func(payload *iaas.CreateImagePayload) { + *request = request.CreateImagePayload(fixtureCreatePayload(func(payload *iaas.CreateImagePayload) { payload.Config.Uefi = utils.Ptr(false) })) }), @@ -377,7 +342,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { @@ -426,11 +391,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/image/delete/delete.go b/internal/cmd/image/delete/delete.go index 10a49e46d..82f5ecbf2 100644 --- a/internal/cmd/image/delete/delete.go +++ b/internal/cmd/image/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { @@ -25,7 +27,7 @@ type inputModel struct { const imageIdArg = "IMAGE_ID" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", imageIdArg), Short: "Deletes an image", @@ -53,18 +55,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - imageName, err := iaasUtils.GetImageName(ctx, apiClient, model.ProjectId, model.ImageId) + imageName, err := iaasUtils.GetImageName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ImageId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get image name: %v", err) imageName = model.ImageId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the image %q for %q?", imageName, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the image %q for %q?", imageName, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -93,19 +93,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM ImageId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteImageRequest { - request := apiClient.DeleteImage(ctx, model.ProjectId, model.ImageId) + request := apiClient.DefaultAPI.DeleteImage(ctx, model.ProjectId, model.Region, model.ImageId) return request } diff --git a/internal/cmd/image/delete/delete_test.go b/internal/cmd/image/delete/delete_test.go index 93b2a656a..88ffcd16d 100644 --- a/internal/cmd/image/delete/delete_test.go +++ b/internal/cmd/image/delete/delete_test.go @@ -4,30 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testImageId = uuid.NewString() ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -37,8 +40,12 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - ImageId: testImageId, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ImageId: testImageId, } for _, mod := range mods { mod(model) @@ -47,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteImageRequest)) iaas.ApiDeleteImageRequest { - request := testClient.DeleteImage(testCtx, testProjectId, testImageId) + request := testClient.DefaultAPI.DeleteImage(testCtx, testProjectId, testRegion, testImageId) for _, mod := range mods { mod(&request) } @@ -57,6 +64,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiDeleteImageRequest)) iaas.ApiD func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string args []string isValid bool @@ -72,14 +80,14 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -105,8 +113,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -137,7 +145,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -174,7 +182,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/image/describe/describe.go b/internal/cmd/image/describe/describe.go index 9a5e5e918..b4c9c21be 100644 --- a/internal/cmd/image/describe/describe.go +++ b/internal/cmd/image/describe/describe.go @@ -2,13 +2,14 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { @@ -27,7 +27,7 @@ type inputModel struct { const imageIdArg = "IMAGE_ID" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", imageIdArg), Short: "Describes image", @@ -69,7 +69,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetImageRequest { - request := apiClient.GetImage(ctx, model.ProjectId, model.ImageId) + request := apiClient.DefaultAPI.GetImage(ctx, model.ProjectId, model.Region, model.ImageId) return request } @@ -84,15 +84,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM ImageId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -100,38 +92,21 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.Image) error if resp == nil { return fmt.Errorf("image not found") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal image: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal image: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { table := tables.NewTable() if id := resp.Id; id != nil { table.AddRow("ID", *id) - } - table.AddSeparator() - - if name := resp.Name; name != nil { - table.AddRow("NAME", *name) table.AddSeparator() } - if format := resp.DiskFormat; format != nil { - table.AddRow("FORMAT", *format) + table.AddRow("NAME", resp.Name) + table.AddSeparator() + if status := resp.Status; status != nil { + table.AddRow("STATUS", *status) table.AddSeparator() } + table.AddRow("FORMAT", resp.DiskFormat) + table.AddSeparator() if diskSize := resp.MinDiskSize; diskSize != nil { table.AddRow("DISK SIZE", *diskSize) table.AddSeparator() @@ -149,11 +124,11 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.Image) error table.AddRow("OPERATING SYSTEM", *os) table.AddSeparator() } - if distro := config.OperatingSystemDistro; distro != nil && distro.IsSet() { + if distro := config.OperatingSystemDistro; distro.IsSet() { table.AddRow("OPERATING SYSTEM DISTRIBUTION", *distro.Get()) table.AddSeparator() } - if version := config.OperatingSystemVersion; version != nil && version.IsSet() { + if version := config.OperatingSystemVersion; version.IsSet() { table.AddRow("OPERATING SYSTEM VERSION", *version.Get()) table.AddSeparator() } @@ -163,9 +138,9 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.Image) error } } - if resp.Labels != nil && len(*resp.Labels) > 0 { + if len(resp.Labels) > 0 { labels := []string{} - for key, value := range *resp.Labels { + for key, value := range resp.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -177,5 +152,5 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.Image) error } return nil - } + }) } diff --git a/internal/cmd/image/describe/describe_test.go b/internal/cmd/image/describe/describe_test.go index a5e421a9d..1c740ef90 100644 --- a/internal/cmd/image/describe/describe_test.go +++ b/internal/cmd/image/describe/describe_test.go @@ -4,30 +4,34 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testImageId = []string{uuid.NewString()} ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -37,8 +41,12 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - ImageId: testImageId[0], + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + ImageId: testImageId[0], } for _, mod := range mods { mod(model) @@ -47,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetImageRequest)) iaas.ApiGetImageRequest { - request := testClient.GetImage(testCtx, testProjectId, testImageId[0]) + request := testClient.DefaultAPI.GetImage(testCtx, testProjectId, testRegion, testImageId[0]) for _, mod := range mods { mod(&request) } @@ -57,6 +65,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiGetImageRequest)) iaas.ApiGetI func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool args []string @@ -78,7 +87,7 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), args: testImageId, isValid: false, @@ -86,7 +95,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), args: testImageId, isValid: false, @@ -94,7 +103,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), args: testImageId, isValid: false, @@ -120,8 +129,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) if err := globalflags.Configure(cmd.Flags()); err != nil { t.Errorf("cannot configure global flags: %v", err) } @@ -148,7 +157,7 @@ func TestParseInput(t *testing.T) { } } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -185,7 +194,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -216,12 +225,38 @@ func TestOutputResult(t *testing.T) { args: args{}, wantErr: true, }, + { + name: "valid value", + args: args{ + resp: &iaas.Image{ + Id: utils.Ptr(uuid.NewString()), + Name: "Image", + Status: utils.Ptr("STATUS"), + DiskFormat: "format", + MinDiskSize: utils.Ptr(int64(0)), + MinRam: utils.Ptr(int64(0)), + Config: &iaas.ImageConfig{ + Architecture: utils.Ptr("architecture"), + OperatingSystem: utils.Ptr("os"), + OperatingSystemDistro: *iaas.NewNullableString(utils.Ptr("os distro")), + OperatingSystemVersion: *iaas.NewNullableString(utils.Ptr("0.00.0")), + Uefi: utils.Ptr(true), + }, + Labels: map[string]any{ + "label1": true, + "label2": false, + "label3": 42, + "foo": "bar", + }, + }, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/image/image.go b/internal/cmd/image/image.go index b722f2b91..65a0cc2a5 100644 --- a/internal/cmd/image/image.go +++ b/internal/cmd/image/image.go @@ -6,15 +6,15 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/image/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/image/list" "github.com/stackitcloud/stackit-cli/internal/cmd/image/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "image", Short: "Manage server images", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( create.NewCmd(params), list.NewCmd(params), diff --git a/internal/cmd/image/list/list.go b/internal/cmd/image/list/list.go index 57280b383..9accc8bbb 100644 --- a/internal/cmd/image/list/list.go +++ b/internal/cmd/image/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,21 +19,22 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { *globalflags.GlobalFlagModel LabelSelector *string Limit *int64 + All *bool } const ( labelSelectorFlag = "label-selector" limitFlag = "limit" + allFlag = "all" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists images", @@ -40,7 +42,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `List all images`, + `List images in your project`, `$ stackit image list`, ), examples.NewExample( @@ -51,10 +53,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List the first 10 images`, `$ stackit image list --limit=10`, ), + examples.NewExample( + `List all images`, + `$ stackit image list --all`, + ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -75,21 +81,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Call API request := buildRequest(ctx, model, apiClient) - response, err := request.Execute() if err != nil { return fmt.Errorf("list images: %w", err) } + items := response.GetItems() - if items := response.GetItems(); len(items) == 0 { - params.Printer.Info("No images found for project %q", projectLabel) - } else { - if model.Limit != nil && len(items) > int(*model.Limit) { - items = (items)[:*model.Limit] - } - if err := outputResult(params.Printer, model.OutputFormat, items); err != nil { - return fmt.Errorf("output images: %w", err) - } + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = (items)[:*model.Limit] + } + if err := outputResult(params.Printer, model.OutputFormat, projectLabel, items); err != nil { + return fmt.Errorf("output images: %w", err) } return nil @@ -103,9 +106,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") + cmd.Flags().Bool(allFlag, false, "List all images available") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,56 +127,42 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), Limit: limit, + All: flags.FlagToBoolPointer(p, cmd, allFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListImagesRequest { - request := apiClient.ListImages(ctx, model.ProjectId) + request := apiClient.DefaultAPI.ListImages(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { request = request.LabelSelector(*model.LabelSelector) } + if model.All != nil { + request = request.All(*model.All) + } return request } -func outputResult(p *print.Printer, outputFormat string, items []iaas.Image) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal image list: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal image list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []iaas.Image) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No images found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.SetHeader("ID", "NAME", "OS", "ARCHITECTURE", "DISTRIBUTION", "VERSION", "LABELS") + table.SetHeader("ID", "NAME", "OS", "ARCHITECTURE", "DISTRIBUTION", "VERSION", "SCOPE", "OWNER", "LABELS") for i := range items { item := items[i] var ( - architecture string = "n/a" - os string = "n/a" - distro string = "n/a" - version string = "n/a" + architecture = "n/a" + os = "n/a" + distro = "n/a" + version = "n/a" + owner = "n/a" + scope = "n/a" ) if cfg := item.Config; cfg != nil { if v := cfg.Architecture; v != nil { @@ -181,20 +171,29 @@ func outputResult(p *print.Printer, outputFormat string, items []iaas.Image) err if v := cfg.OperatingSystem; v != nil { os = *v } - if v := cfg.OperatingSystemDistro; v != nil && v.IsSet() { + if v := cfg.OperatingSystemDistro; v.IsSet() { distro = *v.Get() } - if v := cfg.OperatingSystemVersion; v != nil && v.IsSet() { + if v := cfg.OperatingSystemVersion; v.IsSet() { version = *v.Get() } } + if v := item.GetOwner(); v != "" { + owner = v + } + if v := item.GetScope(); v != "" { + scope = v + } + table.AddRow(utils.PtrString(item.Id), - utils.PtrString(item.Name), + item.Name, os, architecture, distro, version, - utils.JoinStringKeysPtr(*item.Labels, ",")) + scope, + owner, + utils.JoinStringKeysPtr(item.Labels, ",")) } err := table.Display(p) if err != nil { @@ -202,5 +201,5 @@ func outputResult(p *print.Printer, outputFormat string, items []iaas.Image) err } return nil - } + }) } diff --git a/internal/cmd/image/list/list_test.go b/internal/cmd/image/list/list_test.go index 6437127df..ab4a984e6 100644 --- a/internal/cmd/image/list/list_test.go +++ b/internal/cmd/image/list/list_test.go @@ -5,24 +5,27 @@ import ( "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testLabels = "fooKey=fooValue,barKey=barValue,bazKey=bazValue" testLimit int64 = 10 @@ -30,7 +33,9 @@ var ( func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + labelSelectorFlag: testLabels, limitFlag: strconv.Itoa(int(testLimit)), } @@ -42,9 +47,13 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - LabelSelector: utils.Ptr(testLabels), - Limit: &testLimit, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + LabelSelector: utils.Ptr(testLabels), + Limit: &testLimit, } for _, mod := range mods { mod(model) @@ -53,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListImagesRequest)) iaas.ApiListImagesRequest { - request := testClient.ListImages(testCtx, testProjectId) + request := testClient.DefaultAPI.ListImages(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabels) for _, mod := range mods { mod(&request) @@ -64,6 +73,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListImagesRequest)) iaas.ApiLi func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -82,21 +92,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -124,44 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -184,7 +157,7 @@ func TestBuildRequest(t *testing.T) { model.LabelSelector = utils.Ptr("") }), expectedRequest: fixtureRequest(func(request *iaas.ApiListImagesRequest) { - *request = (*request).LabelSelector("") + *request = request.LabelSelector("") }), }, { @@ -193,7 +166,7 @@ func TestBuildRequest(t *testing.T) { model.LabelSelector = utils.Ptr("foo=bar") }), expectedRequest: fixtureRequest(func(request *iaas.ApiListImagesRequest) { - *request = (*request).LabelSelector("foo=bar") + *request = request.LabelSelector("foo=bar") }), }, } @@ -203,7 +176,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -215,6 +188,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []iaas.Image } tests := []struct { @@ -239,11 +213,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/image/update/update.go b/internal/cmd/image/update/update.go index e434f1238..9e8912778 100644 --- a/internal/cmd/image/update/update.go +++ b/internal/cmd/image/update/update.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type imageConfig struct { @@ -58,7 +60,7 @@ type inputModel struct { Id string Name *string DiskFormat *string - Labels *map[string]string + Labels map[string]any Config *imageConfig MinDiskSize *int64 MinRam *int64 @@ -103,7 +105,7 @@ const ( protectedFlag = "protected" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", imageIdArg), Short: "Updates an image", @@ -132,18 +134,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - imageLabel, err := iaasUtils.GetImageName(ctx, apiClient, model.ProjectId, model.Id) + imageLabel, err := iaasUtils.GetImageName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.Id) if err != nil { params.Printer.Debug(print.WarningLevel, "cannot retrieve image name: %v", err) imageLabel = model.Id } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update the image %q?", imageLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update the image %q?", imageLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -153,7 +153,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("update image: %w", err) } - params.Printer.Info("Updated image \"%v\" for %q\n", utils.PtrString(resp.Name), projectLabel) + params.Printer.Info("Updated image \"%v\" for %q\n", resp.Name, projectLabel) return nil }, @@ -203,7 +203,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM Name: flags.FlagToStringPointer(p, cmd, nameFlag), DiskFormat: flags.FlagToStringPointer(p, cmd, diskFormatFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelsFlag), Config: &imageConfig{ Architecture: flags.FlagToStringPointer(p, cmd, architectureFlag), BootMenu: flags.FlagToBoolPointer(p, cmd, bootMenuFlag), @@ -233,25 +233,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM model.Config = nil } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateImageRequest { - request := apiClient.UpdateImage(ctx, model.ProjectId, model.Id) + request := apiClient.DefaultAPI.UpdateImage(ctx, model.ProjectId, model.Region, model.Id) payload := iaas.NewUpdateImagePayload() // Config *ImageConfig `json:"config,omitempty"` payload.DiskFormat = model.DiskFormat - payload.Labels = utils.ConvertStringMapToInterfaceMap(model.Labels) + payload.Labels = model.Labels payload.MinDiskSize = model.MinDiskSize payload.MinRam = model.MinRam payload.Name = model.Name @@ -264,28 +256,28 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli payload.Config.BootMenu = model.Config.BootMenu } if model.Config.CdromBus != nil { - payload.Config.CdromBus = iaas.NewNullableString(model.Config.CdromBus) + payload.Config.CdromBus = *iaas.NewNullableString(model.Config.CdromBus) } if model.Config.DiskBus != nil { - payload.Config.DiskBus = iaas.NewNullableString(model.Config.DiskBus) + payload.Config.DiskBus = *iaas.NewNullableString(model.Config.DiskBus) } if model.Config.NicModel != nil { - payload.Config.NicModel = iaas.NewNullableString(model.Config.NicModel) + payload.Config.NicModel = *iaas.NewNullableString(model.Config.NicModel) } if model.Config.OperatingSystem != nil { payload.Config.OperatingSystem = model.Config.OperatingSystem } if model.Config.OperatingSystemDistro != nil { - payload.Config.OperatingSystemDistro = iaas.NewNullableString(model.Config.OperatingSystemDistro) + payload.Config.OperatingSystemDistro = *iaas.NewNullableString(model.Config.OperatingSystemDistro) } if model.Config.OperatingSystemVersion != nil { - payload.Config.OperatingSystemVersion = iaas.NewNullableString(model.Config.OperatingSystemVersion) + payload.Config.OperatingSystemVersion = *iaas.NewNullableString(model.Config.OperatingSystemVersion) } if model.Config.RescueBus != nil { - payload.Config.RescueBus = iaas.NewNullableString(model.Config.RescueBus) + payload.Config.RescueBus = *iaas.NewNullableString(model.Config.RescueBus) } if model.Config.RescueDevice != nil { - payload.Config.RescueDevice = iaas.NewNullableString(model.Config.RescueDevice) + payload.Config.RescueDevice = *iaas.NewNullableString(model.Config.RescueDevice) } if model.Config.SecureBoot != nil { payload.Config.SecureBoot = model.Config.SecureBoot @@ -294,7 +286,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli payload.Config.Uefi = model.Config.Uefi } if model.Config.VideoModel != nil { - payload.Config.VideoModel = iaas.NewNullableString(model.Config.VideoModel) + payload.Config.VideoModel = *iaas.NewNullableString(model.Config.VideoModel) } if model.Config.VirtioScsi != nil { payload.Config.VirtioScsi = model.Config.VirtioScsi diff --git a/internal/cmd/image/update/update_test.go b/internal/cmd/image/update/update_test.go index 5e4d620ca..25bcba44c 100644 --- a/internal/cmd/image/update/update_test.go +++ b/internal/cmd/image/update/update_test.go @@ -6,24 +6,25 @@ import ( "strings" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testImageId = []string{uuid.NewString()} @@ -50,7 +51,8 @@ var ( func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, nameFlag: testName, diskFormatFlag: testDiskFormat, @@ -78,8 +80,8 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st return flagValues } -func parseLabels(labelstring string) map[string]string { - labels := map[string]string{} +func parseLabels(labelstring string) map[string]any { + labels := map[string]any{} for _, part := range strings.Split(labelstring, ",") { v := strings.Split(part, "=") labels[v[0]] = v[1] @@ -90,11 +92,15 @@ func parseLabels(labelstring string) map[string]string { func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - Id: testImageId[0], - Name: &testName, - DiskFormat: &testDiskFormat, - Labels: utils.Ptr(parseLabels(testLabels)), + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Id: testImageId[0], + Name: &testName, + DiskFormat: &testDiskFormat, + Labels: parseLabels(testLabels), Config: &imageConfig{ BootMenu: &testBootmenu, CdromBus: &testCdRomBus, @@ -124,21 +130,21 @@ func fixtureCreatePayload(mods ...func(payload *iaas.UpdateImagePayload)) (paylo payload = iaas.UpdateImagePayload{ Config: &iaas.ImageConfig{ BootMenu: &testBootmenu, - CdromBus: iaas.NewNullableString(&testCdRomBus), - DiskBus: iaas.NewNullableString(&testDiskBus), - NicModel: iaas.NewNullableString(&testNicModel), + CdromBus: *iaas.NewNullableString(&testCdRomBus), + DiskBus: *iaas.NewNullableString(&testDiskBus), + NicModel: *iaas.NewNullableString(&testNicModel), OperatingSystem: &testOperatingSystem, - OperatingSystemDistro: iaas.NewNullableString(&testOperatingSystemDistro), - OperatingSystemVersion: iaas.NewNullableString(&testOperatingSystemVersion), - RescueBus: iaas.NewNullableString(&testRescueBus), - RescueDevice: iaas.NewNullableString(&testRescueDevice), + OperatingSystemDistro: *iaas.NewNullableString(&testOperatingSystemDistro), + OperatingSystemVersion: *iaas.NewNullableString(&testOperatingSystemVersion), + RescueBus: *iaas.NewNullableString(&testRescueBus), + RescueDevice: *iaas.NewNullableString(&testRescueDevice), SecureBoot: &testSecureBoot, Uefi: &testUefi, - VideoModel: iaas.NewNullableString(&testVideoModel), + VideoModel: *iaas.NewNullableString(&testVideoModel), VirtioScsi: &testVirtioScsi, }, DiskFormat: &testDiskFormat, - Labels: &map[string]interface{}{ + Labels: map[string]any{ "foo": "FOO", "bar": "BAR", "baz": "BAZ", @@ -155,7 +161,7 @@ func fixtureCreatePayload(mods ...func(payload *iaas.UpdateImagePayload)) (paylo } func fixtureRequest(mods ...func(*iaas.ApiUpdateImageRequest)) iaas.ApiUpdateImageRequest { - request := testClient.UpdateImage(testCtx, testProjectId, testImageId[0]) + request := testClient.DefaultAPI.UpdateImage(testCtx, testProjectId, testRegion, testImageId[0]) request = request.UpdateImagePayload(fixtureCreatePayload()) @@ -168,6 +174,7 @@ func fixtureRequest(mods ...func(*iaas.ApiUpdateImageRequest)) iaas.ApiUpdateIma func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string args []string isValid bool @@ -183,7 +190,7 @@ func TestParseInput(t *testing.T) { { description: "no values but valid image id", flagValues: map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, }, args: testImageId, isValid: false, @@ -195,7 +202,7 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), args: testImageId, isValid: false, @@ -203,7 +210,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), args: testImageId, isValid: false, @@ -211,7 +218,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), args: testImageId, isValid: false, @@ -246,7 +253,7 @@ func TestParseInput(t *testing.T) { args: testImageId, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ + model.Labels = map[string]any{ "foo": "bar", } }), @@ -301,8 +308,8 @@ func TestParseInput(t *testing.T) { { description: "update only name", flagValues: map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "foo", + globalflags.ProjectIdFlag: testProjectId, + nameFlag: "foo", }, args: testImageId, isValid: true, @@ -316,8 +323,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) if err := globalflags.Configure(cmd.Flags()); err != nil { t.Errorf("cannot configure global flags: %v", err) } @@ -351,7 +358,7 @@ func TestParseInput(t *testing.T) { } } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -387,7 +394,7 @@ func TestBuildRequest(t *testing.T) { model.Labels = nil }), expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateImageRequest) { - *request = (*request).UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { + *request = request.UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { payload.Labels = nil })) }), @@ -398,7 +405,7 @@ func TestBuildRequest(t *testing.T) { model.Name = utils.Ptr("something else") }), expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateImageRequest) { - *request = (*request).UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { + *request = request.UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { payload.Name = utils.Ptr("something else") })) }), @@ -409,7 +416,7 @@ func TestBuildRequest(t *testing.T) { model.Config.CdromBus = utils.Ptr("something else") }), expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateImageRequest) { - *request = (*request).UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { + *request = request.UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { payload.Config.CdromBus.Set(utils.Ptr("something else")) })) }), @@ -420,7 +427,7 @@ func TestBuildRequest(t *testing.T) { model.Config = nil }), expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateImageRequest) { - *request = (*request).UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { + *request = request.UpdateImagePayload(fixtureCreatePayload(func(payload *iaas.UpdateImagePayload) { payload.Config = nil })) }), @@ -432,7 +439,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest, iaas.NullableString{}), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/key-pair/create/create.go b/internal/cmd/key-pair/create/create.go index a95ee34ec..a3ee15022 100644 --- a/internal/cmd/key-pair/create/create.go +++ b/internal/cmd/key-pair/create/create.go @@ -2,10 +2,10 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -13,9 +13,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -27,11 +26,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel Name *string - PublicKey *string - Labels *map[string]string + PublicKey string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a key pair", @@ -55,9 +54,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit key-pair create --public-key `ssh-rsa xxx` --labels key=value,key1=value1", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,12 +67,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := "Are your sure you want to create a key pair?" - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := "Are your sure you want to create a key pair?" + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -83,7 +80,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create key pair: %w", err) } - return outputResult(params.Printer, model.GlobalFlagModel.OutputFormat, resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -99,34 +96,26 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := inputModel{ GlobalFlagModel: globalFlags, - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), Name: flags.FlagToStringPointer(p, cmd, nameFlag), - PublicKey: flags.FlagToStringPointer(p, cmd, publicKeyFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string fo debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + PublicKey: flags.FlagToStringValue(p, cmd, publicKeyFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateKeyPairRequest { - req := apiClient.CreateKeyPair(ctx) + req := apiClient.DefaultAPI.CreateKeyPair(ctx) payload := iaas.CreateKeyPairPayload{ Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, PublicKey: model.PublicKey, } @@ -138,24 +127,11 @@ func outputResult(p *print.Printer, outputFormat string, item *iaas.Keypair) err return fmt.Errorf("no key pair found") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(item, "", " ") - if err != nil { - return fmt.Errorf("marshal key pair: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(item, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal key pair: %w", err) - } - p.Outputln(string(details)) - default: + return p.OutputResult(outputFormat, item, func() error { p.Outputf("Created key pair %q.\nkey pair Fingerprint: %q\n", utils.PtrString(item.Name), utils.PtrString(item.Fingerprint), ) - } - return nil + return nil + }) } diff --git a/internal/cmd/key-pair/create/create_test.go b/internal/cmd/key-pair/create/create_test.go index 32a0516b6..2e9673360 100644 --- a/internal/cmd/key-pair/create/create_test.go +++ b/internal/cmd/key-pair/create/create_test.go @@ -5,20 +5,20 @@ import ( "os" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testPublicKey = "ssh-rsa " var testKeyPairName = "foobar_key" @@ -40,10 +40,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, }, - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "foo": "bar", - }), - PublicKey: utils.Ptr(testPublicKey), + }, + PublicKey: testPublicKey, Name: utils.Ptr(testKeyPairName), } for _, mod := range mods { @@ -53,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateKeyPairRequest)) iaas.ApiCreateKeyPairRequest { - request := testClient.CreateKeyPair(testCtx) + request := testClient.DefaultAPI.CreateKeyPair(testCtx) request = request.CreateKeyPairPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -63,10 +63,10 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateKeyPairRequest)) iaas.Ap func fixturePayload(mods ...func(payload *iaas.CreateKeyPairPayload)) iaas.CreateKeyPairPayload { payload := iaas.CreateKeyPairPayload{ - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "foo": "bar", - }), - PublicKey: utils.Ptr(testPublicKey), + }, + PublicKey: testPublicKey, Name: utils.Ptr(testKeyPairName), } for _, mod := range mods { @@ -78,6 +78,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateKeyPairPayload)) iaas.Creat func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -111,7 +112,7 @@ func TestParseInput(t *testing.T) { if err != nil { t.Fatal("could not create expected Model", err) } - model.PublicKey = utils.Ptr(string(file)) + model.PublicKey = string(file) }), }, { @@ -123,46 +124,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -186,7 +148,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { @@ -224,11 +186,10 @@ func Test_outputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.item); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.item); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/key-pair/delete/delete.go b/internal/cmd/key-pair/delete/delete.go index 376d21174..b9b452ec2 100644 --- a/internal/cmd/key-pair/delete/delete.go +++ b/internal/cmd/key-pair/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -12,7 +13,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -24,7 +25,7 @@ type inputModel struct { KeyPairName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", keyPairNameArg), Short: "Deletes a key pair", @@ -49,12 +50,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete key pair %q?", model.KeyPairName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete key pair %q?", model.KeyPairName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -82,18 +81,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu KeyPairName: keyPairName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteKeyPairRequest { - return apiClient.DeleteKeyPair(ctx, model.KeyPairName) + return apiClient.DefaultAPI.DeleteKeyPair(ctx, model.KeyPairName) } diff --git a/internal/cmd/key-pair/delete/delete_test.go b/internal/cmd/key-pair/delete/delete_test.go index c5c8dc913..569826fb2 100644 --- a/internal/cmd/key-pair/delete/delete_test.go +++ b/internal/cmd/key-pair/delete/delete_test.go @@ -4,19 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testKeyPairName = "key-pair-name" func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -51,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteKeyPairRequest)) iaas.ApiDeleteKeyPairRequest { - request := testClient.DeleteKeyPair(testCtx, testKeyPairName) + request := testClient.DefaultAPI.DeleteKeyPair(testCtx, testKeyPairName) for _, mod := range mods { mod(&request) } @@ -96,8 +97,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -129,7 +130,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -167,7 +168,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/key-pair/describe/describe.go b/internal/cmd/key-pair/describe/describe.go index 8de535589..4061d2421 100644 --- a/internal/cmd/key-pair/describe/describe.go +++ b/internal/cmd/key-pair/describe/describe.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -19,7 +20,7 @@ import ( "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -36,7 +37,7 @@ type inputModel struct { PublicKey bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", keyPairNameArg), Short: "Describes a key pair", @@ -98,20 +99,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu PublicKey: flags.FlagToBoolValue(p, cmd, publicKeyFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetKeyPairRequest { - return apiClient.GetKeyPair(ctx, model.KeyPairName) + return apiClient.DefaultAPI.GetKeyPair(ctx, model.KeyPairName) } func outputResult(p *print.Printer, outputFormat string, showOnlyPublicKey bool, keyPair iaas.Keypair) error { @@ -120,7 +113,7 @@ func outputResult(p *print.Printer, outputFormat string, showOnlyPublicKey bool, details, err := json.MarshalIndent(keyPair, "", " ") if showOnlyPublicKey { onlyPublicKey := map[string]string{ - "publicKey": *keyPair.PublicKey, + "publicKey": keyPair.PublicKey, } details, err = json.MarshalIndent(onlyPublicKey, "", " ") } @@ -135,7 +128,7 @@ func outputResult(p *print.Printer, outputFormat string, showOnlyPublicKey bool, details, err := yaml.MarshalWithOptions(keyPair, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) if showOnlyPublicKey { onlyPublicKey := map[string]string{ - "publicKey": *keyPair.PublicKey, + "publicKey": keyPair.PublicKey, } details, err = yaml.MarshalWithOptions(onlyPublicKey, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) } @@ -148,16 +141,16 @@ func outputResult(p *print.Printer, outputFormat string, showOnlyPublicKey bool, return nil default: if showOnlyPublicKey { - p.Outputln(*keyPair.PublicKey) + p.Outputln(keyPair.PublicKey) return nil } table := tables.NewTable() table.AddRow("KEY PAIR NAME", utils.PtrString(keyPair.Name)) table.AddSeparator() - if keyPair.Labels != nil && len(*keyPair.Labels) > 0 { + if len(keyPair.Labels) > 0 { var labels []string - for key, value := range *keyPair.Labels { + for key, value := range keyPair.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -167,9 +160,9 @@ func outputResult(p *print.Printer, outputFormat string, showOnlyPublicKey bool, table.AddRow("FINGERPRINT", utils.PtrString(keyPair.Fingerprint)) table.AddSeparator() - truncatedPublicKey := "" - if keyPair.PublicKey != nil { - truncatedPublicKey = (*keyPair.PublicKey)[:maxLengthPublicKey] + "..." + truncatedPublicKey := keyPair.PublicKey + if len(keyPair.PublicKey) > maxLengthPublicKey { + truncatedPublicKey = truncatedPublicKey[:maxLengthPublicKey] + "..." } table.AddRow("PUBLIC KEY", truncatedPublicKey) diff --git a/internal/cmd/key-pair/describe/describe_test.go b/internal/cmd/key-pair/describe/describe_test.go index c3dd4ffaa..69770f1fb 100644 --- a/internal/cmd/key-pair/describe/describe_test.go +++ b/internal/cmd/key-pair/describe/describe_test.go @@ -4,19 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testKeyPairName = "foobar" var testPublicKeyFlag = "true" @@ -52,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetKeyPairRequest)) iaas.ApiGetKeyPairRequest { - request := testClient.GetKeyPair(testCtx, testKeyPairName) + request := testClient.DefaultAPI.GetKeyPair(testCtx, testKeyPairName) for _, mod := range mods { mod(&request) } @@ -108,54 +108,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argsValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argsValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +132,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedResult, cmp.AllowUnexported(tt.expectedResult), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("data does not match: %s", diff) @@ -209,11 +162,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.showOnlyPublicKey, tt.args.keyPair); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.showOnlyPublicKey, tt.args.keyPair); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/key-pair/key-pair.go b/internal/cmd/key-pair/key-pair.go index 44cc1fef8..0ac0c7015 100644 --- a/internal/cmd/key-pair/key-pair.go +++ b/internal/cmd/key-pair/key-pair.go @@ -3,16 +3,17 @@ package keypair import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/key-pair/create" "github.com/stackitcloud/stackit-cli/internal/cmd/key-pair/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/key-pair/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/key-pair/list" "github.com/stackitcloud/stackit-cli/internal/cmd/key-pair/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "key-pair", Short: "Provides functionality for SSH key pairs", @@ -24,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/key-pair/list/list.go b/internal/cmd/key-pair/list/list.go index b614cf5c1..a854e2e2b 100644 --- a/internal/cmd/key-pair/list/list.go +++ b/internal/cmd/key-pair/list/list.go @@ -2,11 +2,11 @@ package list import ( "context" - "encoding/json" "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -18,9 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all key pairs", @@ -58,9 +57,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit key-pair list --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -78,12 +77,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list key pairs: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - params.Printer.Info("No key pairs found\n") - return nil - } + items := resp.GetItems() - items := *resp.Items + // Truncate output if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } @@ -100,7 +96,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) @@ -117,20 +113,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.InfoLevel, modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListKeyPairsRequest { - req := apiClient.ListKeyPairs(ctx) + req := apiClient.DefaultAPI.ListKeyPairs(ctx) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } @@ -138,22 +126,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } func outputResult(p *print.Printer, outputFormat string, keyPairs []iaas.Keypair) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(keyPairs, "", " ") - if err != nil { - return fmt.Errorf("marshal key pairs: %w", err) - } - p.Outputln(string(details)) - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(keyPairs, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal key pairs: %w", err) + return p.OutputResult(outputFormat, keyPairs, func() error { + if len(keyPairs) == 0 { + p.Outputf("No key pairs found\n") + return nil } - p.Outputln(string(details)) - default: table := tables.NewTable() table.SetHeader("KEY PAIR NAME", "LABELS", "FINGERPRINT", "CREATED AT", "UPDATED AT") @@ -162,7 +140,7 @@ func outputResult(p *print.Printer, outputFormat string, keyPairs []iaas.Keypair var labels []string if keyPair.Labels != nil { - for key, value := range *keyPair.Labels { + for key, value := range keyPair.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } } @@ -177,6 +155,6 @@ func outputResult(p *print.Printer, outputFormat string, keyPairs []iaas.Keypair } p.Outputln(table.Render()) - } - return nil + return nil + }) } diff --git a/internal/cmd/key-pair/list/list_test.go b/internal/cmd/key-pair/list/list_test.go index 6679cee0d..5ff89be22 100644 --- a/internal/cmd/key-pair/list/list_test.go +++ b/internal/cmd/key-pair/list/list_test.go @@ -5,20 +5,20 @@ import ( "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "test") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testLabelSelector = "foo=bar" var testLimit = int64(64) @@ -48,7 +48,7 @@ func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListKeyPairsRequest)) iaas.ApiListKeyPairsRequest { - request := testClient.ListKeyPairs(testCtx) + request := testClient.DefaultAPI.ListKeyPairs(testCtx) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -59,6 +59,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListKeyPairsRequest)) iaas.Api func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -116,46 +117,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err = cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatal("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +141,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("request does not match: %s", diff) @@ -212,10 +174,9 @@ func Test_outputResult(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() - if err := outputResult(p, tt.args.outputFormat, tt.args.keyPairs); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.keyPairs); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/key-pair/update/update.go b/internal/cmd/key-pair/update/update.go index b8b7ffbbd..f79169c0c 100644 --- a/internal/cmd/key-pair/update/update.go +++ b/internal/cmd/key-pair/update/update.go @@ -2,10 +2,10 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -14,9 +14,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -26,11 +25,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - Labels *map[string]string + Labels map[string]any KeyPairName *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", keyPairNameArg), Short: "Updates a key pair", @@ -52,12 +51,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update key pair %q?", *model.KeyPairName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return fmt.Errorf("update key pair: %w", err) - } + prompt := fmt.Sprintf("Are you sure you want to update key pair %q?", *model.KeyPairName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return fmt.Errorf("update key pair: %w", err) } // Call API @@ -85,10 +82,10 @@ func configureFlags(cmd *cobra.Command) { } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateKeyPairRequest { - req := apiClient.UpdateKeyPair(ctx, *model.KeyPairName) + req := apiClient.DefaultAPI.UpdateKeyPair(ctx, *model.KeyPairName) payload := iaas.UpdateKeyPairPayload{ - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, } return req.UpdateKeyPairPayload(payload) } @@ -99,42 +96,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) inputM model := inputModel{ GlobalFlagModel: globalFlags, - Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelsFlag), KeyPairName: utils.Ptr(keyPairName), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return model } func outputResult(p *print.Printer, model inputModel, keyPair iaas.Keypair) error { var outputFormat string if model.GlobalFlagModel != nil { - outputFormat = model.GlobalFlagModel.OutputFormat + outputFormat = model.OutputFormat } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(keyPair, "", " ") - if err != nil { - return fmt.Errorf("marshal key pair: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(keyPair, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal key pair: %w", err) - } - p.Outputln(string(details)) - default: + + return p.OutputResult(outputFormat, keyPair, func() error { p.Outputf("Updated labels of key pair %q\n", utils.PtrString(model.KeyPairName)) - } - return nil + return nil + }) } diff --git a/internal/cmd/key-pair/update/update_test.go b/internal/cmd/key-pair/update/update_test.go index 2f5432deb..033ed65db 100644 --- a/internal/cmd/key-pair/update/update_test.go +++ b/internal/cmd/key-pair/update/update_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testKeyPairName = "foobar_key" @@ -46,9 +45,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, }, - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "foo": "bar", - }), + }, KeyPairName: utils.Ptr(testKeyPairName), } for _, mod := range mods { @@ -58,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateKeyPairRequest)) iaas.ApiUpdateKeyPairRequest { - request := testClient.UpdateKeyPair(testCtx, testKeyPairName) + request := testClient.DefaultAPI.UpdateKeyPair(testCtx, testKeyPairName) request = request.UpdateKeyPairPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -68,9 +67,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdateKeyPairRequest)) iaas.Ap func fixturePayload(mods ...func(payload *iaas.UpdateKeyPairPayload)) iaas.UpdateKeyPairPayload { payload := iaas.UpdateKeyPairPayload{ - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]interface{}{ "foo": "bar", - }), + }, } for _, mod := range mods { mod(&payload) @@ -109,8 +108,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -142,7 +141,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model := parseInput(p, cmd, tt.argValues) + model := parseInput(params.Printer, cmd, tt.argValues) if !tt.isValid { t.Fatalf("did not fail on invalid input") @@ -174,7 +173,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { @@ -208,11 +207,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.keyPair); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.keyPair); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/kms/key/create/create.go b/internal/cmd/kms/key/create/create.go new file mode 100644 index 000000000..a829f45ed --- /dev/null +++ b/internal/cmd/kms/key/create/create.go @@ -0,0 +1,199 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + keyRingIdFlag = "keyring-id" + + descriptionFlag = "description" + displayNameFlag = "name" + importOnlyFlag = "import-only" +) + +var ( + algorithmFlag = flags.StringEnumFlag( + "algorithm", + kms.AllowedAlgorithmEnumValues, + "En-/Decryption / signing algorithm.", + ) + purposeFlag = flags.StringEnumFlag( + "purpose", + kms.AllowedPurposeEnumValues, + "Purpose of the key.", + ) + protectionFlag = flags.StringEnumFlag( + "protection", + kms.AllowedProtectionEnumValues, + "The underlying system that is responsible for protecting the key material.") +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + + Algorithm kms.Algorithm + Description *string + Name *string + ImportOnly bool // Default false + Purpose kms.Purpose + Protection kms.Protection +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a KMS key", + Long: "Creates a KMS key.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a symmetric AES key (AES-256) with the name "symm-aes-gcm" under the key ring "my-keyring-id"`, + `$ stackit kms key create --keyring-id "my-keyring-id" --algorithm "aes_256_gcm" --name "symm-aes-gcm" --purpose "symmetric_encrypt_decrypt" --protection "software"`), + examples.NewExample( + `Create an asymmetric RSA encryption key (RSA-2048)`, + `$ stackit kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "prod-orders-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software"`), + examples.NewExample( + `Create a message authentication key (HMAC-SHA512)`, + `$ stackit kms key create --keyring-id "my-keyring-id" --algorithm "hmac_sha512" --name "api-mac-key" --purpose "message_authentication_code" --protection "software"`), + examples.NewExample( + `Create an ECDSA P-256 key for signing & verification`, + `$ stackit kms key create --keyring-id "my-keyring-id" --algorithm "ecdsa_p256_sha256" --name "signing-ecdsa-p256" --purpose "asymmetric_sign_verify" --protection "software"`), + examples.NewExample( + `Create an import-only key (versions must be imported)`, + `$ stackit kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "ext-managed-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --import-only`), + examples.NewExample( + `Create a key and print the result as YAML`, + `$ stackit kms key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256" --name "yaml-output-rsa" --purpose "asymmetric_encrypt_decrypt" --protection "software" --output yaml`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS Key?") + if err != nil { + return err + } + + // Call API + req, _ := buildRequest(ctx, model, apiClient.DefaultAPI) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create KMS key: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating key", func() error { + _, err = wait.CreateOrUpdateKeyWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, resp.Id).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for KMS key creation: %w", err) + } + } + + return outputResult(params.Printer, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + Algorithm: algorithmFlag.Get(), + Name: flags.FlagToStringPointer(p, cmd, displayNameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + ImportOnly: flags.FlagToBoolValue(p, cmd, importOnlyFlag), + Purpose: purposeFlag.Get(), + Protection: protectionFlag.Get(), + } + + p.DebugInputModel(model) + return &model, nil +} + +type kmsKeyClient interface { + CreateKey(ctx context.Context, projectId string, regionId string, keyRingId string) kms.ApiCreateKeyRequest +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient kmsKeyClient) (kms.ApiCreateKeyRequest, error) { + req := apiClient.CreateKey(ctx, model.ProjectId, model.Region, model.KeyRingId) + + req = req.CreateKeyPayload(kms.CreateKeyPayload{ + DisplayName: utils.PtrString(model.Name), + Description: model.Description, + Algorithm: model.Algorithm, + Purpose: model.Purpose, + ImportOnly: &model.ImportOnly, + Protection: model.Protection, + }) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *kms.Key) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s the KMS key %q. Key ID: %s\n", operationState, resp.DisplayName, resp.Id) + return nil + }) +} + +func configureFlags(cmd *cobra.Command) { + algorithmFlag.Register(cmd.Flags()) + purposeFlag.Register(cmd.Flags()) + protectionFlag.Register(cmd.Flags()) + + // All further non Enum Flags + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().String(displayNameFlag, "", "The display name to distinguish multiple keys") + cmd.Flags().String(descriptionFlag, "", "Optional description of the key") + cmd.Flags().Bool(importOnlyFlag, false, "States whether versions can be created or only imported") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, algorithmFlag.Name(), purposeFlag.Name(), displayNameFlag, protectionFlag.Name()) + cobra.CheckErr(err) +} diff --git a/internal/cmd/kms/key/create/create_test.go b/internal/cmd/kms/key/create/create_test.go new file mode 100644 index 000000000..94978102f --- /dev/null +++ b/internal/cmd/kms/key/create/create_test.go @@ -0,0 +1,328 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testRegion = "eu01" + testAlgorithm = kms.ALGORITHM_RSA_2048_OAEP_SHA256 + testDisplayName = "my-key" + testPurpose = kms.PURPOSE_ASYMMETRIC_ENCRYPT_DECRYPT + testDescription = "my key description" + testImportOnly = "true" + testProtection = kms.PROTECTION_SOFTWARE +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + algorithmFlag.Name(): string(testAlgorithm), + displayNameFlag: testDisplayName, + purposeFlag.Name(): string(testPurpose), + descriptionFlag: testDescription, + importOnlyFlag: testImportOnly, + protectionFlag.Name(): string(testProtection), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + Algorithm: testAlgorithm, + Name: utils.Ptr(testDisplayName), + Purpose: testPurpose, + Description: utils.Ptr(testDescription), + ImportOnly: true, // Watch out: ImportOnly is not testImportOnly! + Protection: testProtection, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiCreateKeyRequest)) kms.ApiCreateKeyRequest { + request := testClient.DefaultAPI.CreateKey(testCtx, testProjectId, testRegion, testKeyRingId) + request = request.CreateKeyPayload(kms.CreateKeyPayload{ + Algorithm: testAlgorithm, + DisplayName: testDisplayName, + Purpose: testPurpose, + Description: utils.Ptr(testDescription), + ImportOnly: utils.Ptr(true), + Protection: testProtection, + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "optional flags omitted", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + delete(flagValues, importOnlyFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.ImportOnly = false + }), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "algorithm missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, algorithmFlag.Name()) + }), + isValid: false, + }, + { + description: "protection missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, protectionFlag.Name()) + }), + isValid: false, + }, + { + description: "name missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + { + description: "purpose missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, purposeFlag.Name()) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiCreateKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.ImportOnly = false + }), + expectedRequest: fixtureRequest().CreateKeyPayload(kms.CreateKeyPayload{ + Algorithm: testAlgorithm, + DisplayName: testDisplayName, + Purpose: testPurpose, + Description: nil, + ImportOnly: utils.Ptr(false), + Protection: testProtection, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + if err != nil { + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + key *kms.Key + wantErr bool + }{ + { + description: "nil response", + key: nil, + wantErr: true, + }, + { + description: "default output", + key: &kms.Key{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wantErr: false, + }, + { + description: "json output", + key: &kms.Key{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wantErr: false, + }, + { + description: "yaml output", + key: &kms.Key{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/key/delete/delete.go b/internal/cmd/kms/key/delete/delete.go new file mode 100644 index 000000000..cfef2759f --- /dev/null +++ b/internal/cmd/kms/key/delete/delete.go @@ -0,0 +1,134 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" +) + +const ( + keyIdArg = "KEY_ID" + + keyRingIdFlag = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyId string + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", keyIdArg), + Short: "Deletes a KMS key", + Long: "Deletes a KMS key inside a specific key ring.", + Args: args.SingleArg(keyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id"`, + `$ stackit kms key delete "MY_KEY_ID" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + keyName, err := kmsUtils.GetKeyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key name: %v", err) + keyName = model.KeyId + } + + prompt := fmt.Sprintf("Are you sure you want to delete key %q? (This cannot be undone)", keyName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete KMS key: %w", err) + } + + // Don't wait for a month until the deletion was performed. + // Just print the deletion date. + resp, err := apiClient.DefaultAPI.GetKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key: %v", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + keyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: keyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDeleteKeyRequest { + req := apiClient.DefaultAPI.DeleteKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + return req +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, resp *kms.Key) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Deletion of KMS key %s scheduled successfully for the deletion date: %s\n", resp.DisplayName, utils.PtrString(resp.DeletionDate)) + return nil + }) +} diff --git a/internal/cmd/kms/key/delete/delete_test.go b/internal/cmd/kms/key/delete/delete_test.go new file mode 100644 index 000000000..c98e56814 --- /dev/null +++ b/internal/cmd/kms/key/delete/delete_test.go @@ -0,0 +1,294 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testKeyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiDeleteKeyRequest)) kms.ApiDeleteKeyRequest { + request := testClient.DefaultAPI.DeleteKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (keyId)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiDeleteKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + wantErr bool + outputFormat string + resp *kms.Key + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Key{}, + wantErr: false, + }, + { + description: "json output", + outputFormat: print.JSONOutputFormat, + resp: &kms.Key{}, + wantErr: false, + }, + { + description: "yaml output", + outputFormat: print.YAMLOutputFormat, + resp: &kms.Key{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/key/describe/describe.go b/internal/cmd/kms/key/describe/describe.go new file mode 100644 index 000000000..05c1e63db --- /dev/null +++ b/internal/cmd/kms/key/describe/describe.go @@ -0,0 +1,133 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + argKeyID = "KEY_ID" + flagKeyRingID = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyID string + KeyRingID string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argKeyID), + Short: "Describe a KMS key", + Long: "Describe a KMS key", + Args: args.SingleArg(argKeyID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS key with ID xxx of keyring yyy`, + `$ stackit kms key describe xxx --keyring-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID") + err := flags.MarkFlagsRequired(cmd, flagKeyRingID) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + KeyID: inputArgs[0], + KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRequest { + return apiClient.DefaultAPI.GetKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.KeyID) +} + +func outputResult(p *print.Printer, outputFormat string, key *kms.Key) error { + if key == nil { + return fmt.Errorf("key response is empty") + } + return p.OutputResult(outputFormat, key, func() error { + table := tables.NewTable() + table.AddRow("ID", key.Id) + table.AddSeparator() + table.AddRow("DISPLAY NAME", key.DisplayName) + table.AddSeparator() + table.AddRow("CREATED AT", key.CreatedAt) + table.AddSeparator() + table.AddRow("STATE", key.State) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(key.Description)) + table.AddSeparator() + table.AddRow("ACCESS SCOPE", key.AccessScope) + table.AddSeparator() + table.AddRow("ALGORITHM", key.Algorithm) + table.AddSeparator() + table.AddRow("DELETION DATE", utils.PtrString(key.DeletionDate)) + table.AddSeparator() + table.AddRow("IMPORT ONLY", key.ImportOnly) + table.AddSeparator() + table.AddRow("KEYRING ID", key.KeyRingId) + table.AddSeparator() + table.AddRow("PROTECTION", key.Protection) + table.AddSeparator() + table.AddRow("PURPOSE", key.Purpose) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/kms/key/describe/describe_test.go b/internal/cmd/kms/key/describe/describe_test.go new file mode 100644 index 000000000..0e863f2d2 --- /dev/null +++ b/internal/cmd/kms/key/describe/describe_test.go @@ -0,0 +1,219 @@ +package describe + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testKeyID = uuid.NewString() +var testTime = time.Time{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flagKeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyID: testKeyID, + KeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: []string{testKeyID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing key ring id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, flagKeyRingID) }), + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { + m[flagKeyRingID] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testKeyID}, + flagValues: fixtureFlagValues(func(m map[string]string) { m[globalflags.ProjectIdFlag] = "invalid-uuid" }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.DefaultAPI.GetKey(testCtx, testProjectId, testRegion, testKeyRingID, testKeyID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.Key + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.Key{ + AccessScope: kms.ACCESSSCOPE_PUBLIC, + Algorithm: kms.ALGORITHM_AES_256_GCM, + CreatedAt: testTime, + DeletionDate: nil, + Description: utils.Ptr("very secure and secret key"), + DisplayName: "Test Key", + Id: testKeyID, + ImportOnly: true, + KeyRingId: testKeyRingID, + Protection: kms.PROTECTION_SOFTWARE, + Purpose: kms.PURPOSE_SYMMETRIC_ENCRYPT_DECRYPT, + State: kms.KEYSTATE_ACTIVE, + }, + expected: fmt.Sprintf(` + ID │ %-37s +───────────────┼────────────────────────────────────── + DISPLAY NAME │ Test Key +───────────────┼────────────────────────────────────── + CREATED AT │ %-37s +───────────────┼────────────────────────────────────── + STATE │ active +───────────────┼────────────────────────────────────── + DESCRIPTION │ very secure and secret key +───────────────┼────────────────────────────────────── + ACCESS SCOPE │ PUBLIC +───────────────┼────────────────────────────────────── + ALGORITHM │ aes_256_gcm +───────────────┼────────────────────────────────────── + DELETION DATE │ +───────────────┼────────────────────────────────────── + IMPORT ONLY │ true +───────────────┼────────────────────────────────────── + KEYRING ID │ %-37s +───────────────┼────────────────────────────────────── + PROTECTION │ software +───────────────┼────────────────────────────────────── + PURPOSE │ symmetric_encrypt_decrypt + +`, + testKeyID, + testTime, + testKeyRingID, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(params.Out.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/kms/key/importKey/importKey.go b/internal/cmd/kms/key/importKey/importKey.go new file mode 100644 index 000000000..0c3c0012e --- /dev/null +++ b/internal/cmd/kms/key/importKey/importKey.go @@ -0,0 +1,164 @@ +package importKey + +import ( + "context" + "encoding/base64" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + keyIdArg = "KEY_ID" + + keyRingIdFlag = "keyring-id" + wrappedKeyFlag = "wrapped-key" + wrappingKeyIdFlag = "wrapping-key-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + KeyId string + WrappedKey string + WrappingKeyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("import %s", keyIdArg), + Short: "Import a KMS key", + Long: "After encrypting the secret with the wrapping key’s public key and Base64-encoding it, import it as a new version of the specified KMS key.", + Args: args.SingleArg(keyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Import a new version for the given KMS key "MY_KEY_ID" from literal value`, + `$ stackit kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "BASE64_VALUE" --wrapping-key-id "MY_WRAPPING_KEY_ID"`), + examples.NewExample( + `Import from a file`, + `$ stackit kms key import "MY_KEY_ID" --keyring-id "my-keyring-id" --wrapped-key "@path/to/wrapped.key.b64" --wrapping-key-id "MY_WRAPPING_KEY_ID"`, + ), + ), + + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + keyName, err := kmsUtils.GetKeyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key name: %v", err) + keyName = model.KeyId + } + keyRingName, err := kmsUtils.GetKeyRingName(ctx, apiClient.DefaultAPI, model.ProjectId, model.KeyRingId, model.Region) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key ring name: %v", err) + keyRingName = model.KeyRingId + } + + prompt := fmt.Sprintf("Are you sure you want to import a new version for the KMS Key %q inside the key ring %q?", keyName, keyRingName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req, _ := buildRequest(ctx, model, apiClient.DefaultAPI) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("import KMS key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, keyRingName, keyName, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + keyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // WrappedKey needs to be base64 encoded + var wrappedKey = flags.FlagToStringValue(p, cmd, wrappedKeyFlag) + _, err := base64.StdEncoding.DecodeString(wrappedKey) + if err != nil || wrappedKey == "" { + return nil, &cliErr.FlagValidationError{ + Flag: wrappedKeyFlag, + Details: "The 'wrappedKey' argument is required and needs to be base64 encoded (whether provided inline or via file).", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyId: keyId, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + WrappedKey: wrappedKey, + WrappingKeyId: flags.FlagToStringValue(p, cmd, wrappingKeyIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +type kmsKeyClient interface { + ImportKey(ctx context.Context, projectId string, regionId string, keyRingId string, keyId string) kms.ApiImportKeyRequest +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient kmsKeyClient) (kms.ApiImportKeyRequest, error) { + req := apiClient.ImportKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + + req = req.ImportKeyPayload(kms.ImportKeyPayload{ + WrappedKey: model.WrappedKey, + WrappingKeyId: model.WrappingKeyId, + }) + return req, nil +} + +func outputResult(p *print.Printer, outputFormat, keyRingName, keyName string, resp *kms.Version) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Imported a new version for the key %q inside the key ring %q\n", keyName, keyRingName) + return nil + }) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().Var(flags.ReadFromFileFlag(), wrappedKeyFlag, "The wrapped key material to be imported. Base64-encoded. Pass the value directly or a file path (e.g. @path/to/wrapped.key.b64)") + cmd.Flags().Var(flags.UUIDFlag(), wrappingKeyIdFlag, "The unique id of the wrapping key the key material has been wrapped with") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, wrappedKeyFlag, wrappingKeyIdFlag) + cobra.CheckErr(err) +} diff --git a/internal/cmd/kms/key/importKey/importKey_test.go b/internal/cmd/kms/key/importKey/importKey_test.go new file mode 100644 index 000000000..1c7d519e6 --- /dev/null +++ b/internal/cmd/kms/key/importKey/importKey_test.go @@ -0,0 +1,364 @@ +package importKey + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() + testWrappingKeyId = uuid.NewString() + testWrappedKey = "SnVzdCBzYXlpbmcgaGV5Oyk=" +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testKeyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + wrappedKeyFlag: testWrappedKey, + wrappingKeyIdFlag: testWrappingKeyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + WrappedKey: testWrappedKey, + WrappingKeyId: testWrappingKeyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiImportKeyRequest)) kms.ApiImportKeyRequest { + request := testClient.DefaultAPI.ImportKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId) + request = request.ImportKeyPayload(kms.ImportKeyPayload{ + WrappedKey: testWrappedKey, + WrappingKeyId: testWrappingKeyId, + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no args (keyId)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values provided", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing (required)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: []string{"invalid-key"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "wrapping key id missing (required)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, wrappingKeyIdFlag) + }), + isValid: false, + }, + { + description: "wrapping key id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[wrappingKeyIdFlag] = "" + }), + isValid: false, + }, + { + description: "wrapping key id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[wrappingKeyIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "wrapped key missing (required)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, wrappedKeyFlag) + }), + isValid: false, + }, + { + description: "wrapped key invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[wrappedKeyFlag] = "" + }), + isValid: false, + }, + { + description: "wrapped key invalid 2 - not base64", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[wrappedKeyFlag] = "Not Base 64" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiImportKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + if err != nil { + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + version *kms.Version + outputFormat string + keyRingName string + keyName string + wantErr bool + }{ + { + description: "nil response", + version: nil, + wantErr: true, + }, + { + description: "default output", + version: &kms.Version{}, + keyRingName: "my-key-ring", + keyName: "my-key", + wantErr: false, + }, + { + description: "json output", + version: &kms.Version{}, + outputFormat: print.JSONOutputFormat, + keyRingName: "my-key-ring", + keyName: "my-key", + wantErr: false, + }, + { + description: "yaml output", + version: &kms.Version{}, + outputFormat: print.YAMLOutputFormat, + keyRingName: "my-key-ring", + keyName: "my-key", + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.keyRingName, tt.keyName, tt.version) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/key/key.go b/internal/cmd/kms/key/key.go new file mode 100644 index 000000000..4ebf30501 --- /dev/null +++ b/internal/cmd/kms/key/key.go @@ -0,0 +1,38 @@ +package key + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/importKey" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/restore" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key/rotate" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "key", + Short: "Manage KMS keys", + Long: "Provides functionality for key operations inside the KMS", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(importKey.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(restore.NewCmd(params)) + cmd.AddCommand(rotate.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/kms/key/list/list.go b/internal/cmd/kms/key/list/list.go new file mode 100644 index 000000000..4c57f24a3 --- /dev/null +++ b/internal/cmd/kms/key/list/list.go @@ -0,0 +1,133 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + keyRingIdFlag = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all KMS keys", + Long: "List all KMS keys inside a key ring.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all KMS keys for the key ring "my-keyring-id"`, + `$ stackit kms key list --keyring-id "my-keyring-id"`), + examples.NewExample( + `List all KMS keys in JSON format`, + `$ stackit kms key list --keyring-id "my-keyring-id" --output-format json`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get KMS Keys: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.ProjectId, model.KeyRingId, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListKeysRequest { + req := apiClient.DefaultAPI.ListKeys(ctx, model.ProjectId, model.Region, model.KeyRingId) + return req +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat, projectId, keyRingId string, resp *kms.KeyList) error { + if resp == nil || resp.Keys == nil { + return fmt.Errorf("response was nil / empty") + } + + keys := resp.Keys + + return p.OutputResult(outputFormat, keys, func() error { + if len(keys) == 0 { + p.Outputf("No keys found for project %q under the key ring %q\n", projectId, keyRingId) + return nil + } + table := tables.NewTable() + table.SetHeader("ID", "NAME", "SCOPE", "ALGORITHM", "DELETION DATE", "STATUS") + + for _, key := range keys { + table.AddRow( + key.Id, + key.DisplayName, + key.Purpose, + key.Algorithm, + utils.PtrString(key.DeletionDate), + key.State, + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/kms/key/list/list_test.go b/internal/cmd/kms/key/list/list_test.go new file mode 100644 index 000000000..089baad1c --- /dev/null +++ b/internal/cmd/kms/key/list/list_test.go @@ -0,0 +1,259 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiListKeysRequest)) kms.ApiListKeysRequest { + request := testClient.DefaultAPI.ListKeys(testCtx, testProjectId, testRegion, testKeyRingId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "missing keyRingId", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "invalid keyRingId 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid keyRingId 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "Not a valid uuid" + }), + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiListKeysRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + resp *kms.KeyList + projectId string + keyRingId string + outputFormat string + wantErr bool + }{ + { + description: "nil response", + resp: nil, + projectId: uuid.NewString(), + keyRingId: uuid.NewString(), + wantErr: true, + }, + { + description: "empty response", + resp: &kms.KeyList{}, + projectId: uuid.NewString(), + keyRingId: uuid.NewString(), + wantErr: true, + }, + { + description: "default output", + resp: &kms.KeyList{Keys: []kms.Key{}}, + projectId: uuid.NewString(), + keyRingId: uuid.NewString(), + wantErr: false, + }, + { + description: "json output", + resp: &kms.KeyList{Keys: []kms.Key{}}, + projectId: uuid.NewString(), + keyRingId: uuid.NewString(), + outputFormat: print.JSONOutputFormat, + wantErr: false, + }, + { + description: "yaml output", + resp: &kms.KeyList{Keys: []kms.Key{}}, + projectId: uuid.NewString(), + keyRingId: uuid.NewString(), + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.projectId, tt.keyRingId, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/key/restore/restore.go b/internal/cmd/kms/key/restore/restore.go new file mode 100644 index 000000000..5849ec00c --- /dev/null +++ b/internal/cmd/kms/key/restore/restore.go @@ -0,0 +1,133 @@ +package restore + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" +) + +const ( + keyIdArg = "KEY_ID" + + keyRingIdFlag = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyId string + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("restore %s", keyIdArg), + Short: "Restore a key", + Long: "Restores the given key from deletion.", + Args: args.SingleArg(keyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Restore a KMS key "MY_KEY_ID" inside the key ring "my-keyring-id" that was scheduled for deletion.`, + `$ stackit kms key restore "MY_KEY_ID" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + keyName, err := kmsUtils.GetKeyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key name: %v", err) + keyName = model.KeyId + } + + prompt := fmt.Sprintf("Are you sure you want to restore key %q? (This cannot be undone)", keyName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("restore KMS key: %w", err) + } + + // Grab the key after the restore was applied to display the new state to the user. + resp, err := apiClient.DefaultAPI.GetKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key: %v", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + keyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: keyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiRestoreKeyRequest { + req := apiClient.DefaultAPI.RestoreKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + return req +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, resp *kms.Key) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Successfully restored KMS key %q\n", resp.DisplayName) + return nil + }) +} diff --git a/internal/cmd/kms/key/restore/restore_test.go b/internal/cmd/kms/key/restore/restore_test.go new file mode 100644 index 000000000..2478b9ce6 --- /dev/null +++ b/internal/cmd/kms/key/restore/restore_test.go @@ -0,0 +1,294 @@ +package restore + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testKeyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiRestoreKeyRequest)) kms.ApiRestoreKeyRequest { + request := testClient.DefaultAPI.RestoreKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (keyId)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiRestoreKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + wantErr bool + outputFormat string + resp *kms.Key + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Key{}, + wantErr: false, + }, + { + description: "json output", + outputFormat: print.JSONOutputFormat, + resp: &kms.Key{}, + wantErr: false, + }, + { + description: "yaml output", + outputFormat: print.YAMLOutputFormat, + resp: &kms.Key{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/key/rotate/rotate.go b/internal/cmd/kms/key/rotate/rotate.go new file mode 100644 index 000000000..8d265912f --- /dev/null +++ b/internal/cmd/kms/key/rotate/rotate.go @@ -0,0 +1,127 @@ +package rotate + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + keyIdArg = "KEY_ID" + + keyRingIdFlag = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyId string + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("rotate %s", keyIdArg), + Short: "Rotate a key", + Long: "Rotates the given key.", + Args: args.SingleArg(keyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Rotate a KMS key "MY_KEY_ID" and increase its version inside the key ring "my-keyring-id".`, + `$ stackit kms key rotate "MY_KEY_ID" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + keyName, err := kmsUtils.GetKeyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key name: %v", err) + keyName = model.KeyId + } + + prompt := fmt.Sprintf("Are you sure you want to rotate the key %q? (this cannot be undone)", keyName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("rotate KMS key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + keyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: keyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiRotateKeyRequest { + req := apiClient.DefaultAPI.RotateKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) + return req +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Rotated key %s\n", resp.KeyId) + return nil + }) +} diff --git a/internal/cmd/kms/key/rotate/rotate_test.go b/internal/cmd/kms/key/rotate/rotate_test.go new file mode 100644 index 000000000..7b922a81e --- /dev/null +++ b/internal/cmd/kms/key/rotate/rotate_test.go @@ -0,0 +1,294 @@ +package rotate + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testKeyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiRotateKeyRequest)) kms.ApiRotateKeyRequest { + request := testClient.DefaultAPI.RotateKey(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (keyId)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiRotateKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + resp *kms.Version + outputFormat string + wantErr bool + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "json output", + resp: &kms.Version{}, + outputFormat: print.JSONOutputFormat, + wantErr: false, + }, + { + description: "yaml output", + resp: &kms.Version{}, + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/keyring/create/create.go b/internal/cmd/kms/keyring/create/create.go new file mode 100644 index 000000000..d9b3b76d6 --- /dev/null +++ b/internal/cmd/kms/keyring/create/create.go @@ -0,0 +1,165 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + keyRingNameFlag = "name" + descriptionFlag = "description" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyringName string + Description string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a KMS key ring", + Long: "Creates a KMS key ring.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a KMS key ring with name "my-keyring"`, + "$ stackit kms keyring create --name my-keyring"), + examples.NewExample( + `Create a KMS key ring with a description`, + "$ stackit kms keyring create --name my-keyring --description my-description"), + examples.NewExample( + `Create a KMS key ring and print the result as YAML`, + "$ stackit kms keyring create --name my-keyring -o yaml"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS key ring?") + if err != nil { + return err + } + + // Call API + req, _ := buildRequest(ctx, model, apiClient.DefaultAPI) + + keyRing, err := req.Execute() + if err != nil { + return fmt.Errorf("create KMS key ring: %w", err) + } + + // Prevent potential nil pointer dereference + if keyRing == nil { + return fmt.Errorf("API call succeeded but returned an invalid response (missing key ring ID)") + } + + keyRingId := keyRing.Id + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating key ring", func() error { + _, err = wait.CreateKeyRingWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, keyRingId).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for KMS key ring creation: %w", err) + } + } + + return outputResult(params.Printer, model, keyRing) + }, + } + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + keyringName := flags.FlagToStringValue(p, cmd, keyRingNameFlag) + + if keyringName == "" { + return nil, &cliErr.DSAInputPlanError{ + Cmd: cmd, + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyringName: keyringName, + Description: flags.FlagToStringValue(p, cmd, descriptionFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +type kmsKeyringClient interface { + CreateKeyRing(ctx context.Context, projectId string, regionId string) kms.ApiCreateKeyRingRequest +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient kmsKeyringClient) (kms.ApiCreateKeyRingRequest, error) { + req := apiClient.CreateKeyRing(ctx, model.ProjectId, model.Region) + + req = req.CreateKeyRingPayload(kms.CreateKeyRingPayload{ + DisplayName: model.KeyringName, + + // Description should be empty by default and only be overwritten with the descriptionFlag if it was passed. + Description: &model.Description, + }) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *kms.KeyRing) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s key ring. KMS key ring ID: %s\n", operationState, resp.Id) + return nil + }) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(keyRingNameFlag, "", "Name of the KMS key ring") + cmd.Flags().String(descriptionFlag, "", "Optional description of the key ring") + + err := flags.MarkFlagsRequired(cmd, keyRingNameFlag) + cobra.CheckErr(err) +} diff --git a/internal/cmd/kms/keyring/create/create_test.go b/internal/cmd/kms/keyring/create/create_test.go new file mode 100644 index 000000000..c74c35753 --- /dev/null +++ b/internal/cmd/kms/keyring/create/create_test.go @@ -0,0 +1,250 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testRegion = "eu01" + testKeyRingName = "my-key-ring" + testDescription = "my-description" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingNameFlag: testKeyRingName, + descriptionFlag: testDescription, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyringName: testKeyRingName, + Description: testDescription, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiCreateKeyRingRequest)) kms.ApiCreateKeyRingRequest { + request := testClient.DefaultAPI.CreateKeyRing(testCtx, testProjectId, testRegion) + request = request.CreateKeyRingPayload(kms.CreateKeyRingPayload{ + DisplayName: testKeyRingName, + Description: utils.Ptr(testDescription), + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "optional flags omitted", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = "" + }), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiCreateKeyRingRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + if err != nil { + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + model *inputModel + description string + keyRing *kms.KeyRing + wantErr bool + }{ + { + description: "nil response", + keyRing: nil, + wantErr: true, + }, + { + description: "default output", + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + keyRing: &kms.KeyRing{}, + wantErr: false, + }, + { + description: "json output", + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + keyRing: &kms.KeyRing{}, + wantErr: false, + }, + { + description: "yaml output", + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + keyRing: &kms.KeyRing{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, tt.keyRing) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/keyring/delete/delete.go b/internal/cmd/kms/keyring/delete/delete.go new file mode 100644 index 000000000..cb70e54de --- /dev/null +++ b/internal/cmd/kms/keyring/delete/delete.go @@ -0,0 +1,106 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + keyRingIdArg = "KEYRING-ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", keyRingIdArg), + Short: "Deletes a KMS key ring", + Long: "Deletes a KMS key ring.", + Args: args.SingleArg(keyRingIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a KMS key ring with ID "MY_KEYRING_ID"`, + `$ stackit kms keyring delete "MY_KEYRING_ID"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + keyRingLabel, err := kmsUtils.GetKeyRingName(ctx, apiClient.DefaultAPI, model.ProjectId, model.KeyRingId, model.Region) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key ring name: %v", err) + keyRingLabel = model.KeyRingId + } + + prompt := fmt.Sprintf("Are you sure you want to delete key ring %q? (this cannot be undone)", keyRingLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete KMS key ring: %w", err) + } + + // No async wait required; key ring deletion is synchronous. + + // Don't output anything. It's a deletion. + params.Printer.Info("Deleted the key ring %q\n", keyRingLabel) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + keyRingId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: keyRingId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDeleteKeyRingRequest { + req := apiClient.DefaultAPI.DeleteKeyRing(ctx, model.ProjectId, model.Region, model.KeyRingId) + return req +} diff --git a/internal/cmd/kms/keyring/delete/delete_test.go b/internal/cmd/kms/keyring/delete/delete_test.go new file mode 100644 index 000000000..638f02f90 --- /dev/null +++ b/internal/cmd/kms/keyring/delete/delete_test.go @@ -0,0 +1,213 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testKeyRingId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiDeleteKeyRingRequest)) kms.ApiDeleteKeyRingRequest { + request := testClient.DefaultAPI.DeleteKeyRing(testCtx, testProjectId, testRegion, testKeyRingId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no args (keyRingId)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid keyRingId", + argValues: fixtureArgValues(func(argValues []string) { + argValues[0] = "Not an uuid" + }), + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiDeleteKeyRingRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/kms/keyring/describe/describe.go b/internal/cmd/kms/keyring/describe/describe.go new file mode 100644 index 000000000..73f7db3d6 --- /dev/null +++ b/internal/cmd/kms/keyring/describe/describe.go @@ -0,0 +1,108 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + argKeyRingID = "KEYRING_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingID string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argKeyRingID), + Short: "Describe a KMS key ring", + Long: "Describe a KMS key ring", + Args: args.SingleArg(argKeyRingID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS key ring with ID xxx`, + `$ stackit kms keyring describe xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get key ring: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + KeyRingID: inputArgs[0], + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetKeyRingRequest { + return apiClient.DefaultAPI.GetKeyRing(ctx, model.ProjectId, model.Region, model.KeyRingID) +} + +func outputResult(p *print.Printer, outputFormat string, keyRing *kms.KeyRing) error { + if keyRing == nil { + return fmt.Errorf("key ring response is empty") + } + return p.OutputResult(outputFormat, keyRing, func() error { + table := tables.NewTable() + table.AddRow("ID", keyRing.Id) + table.AddSeparator() + table.AddRow("DISPLAY NAME", keyRing.DisplayName) + table.AddSeparator() + table.AddRow("CREATED AT", keyRing.CreatedAt) + table.AddSeparator() + table.AddRow("STATE", keyRing.State) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(keyRing.Description)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/kms/keyring/describe/describe_test.go b/internal/cmd/kms/keyring/describe/describe_test.go new file mode 100644 index 000000000..ff2317b30 --- /dev/null +++ b/internal/cmd/kms/keyring/describe/describe_test.go @@ -0,0 +1,180 @@ +package describe + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testTime = time.Time{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: []string{testKeyRingID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{"!invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testKeyRingID}, + flagValues: fixtureFlagValues(func(m map[string]string) { m[globalflags.ProjectIdFlag] = "invalid-uuid" }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.DefaultAPI.GetKeyRing(testCtx, testProjectId, testRegion, testKeyRingID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.KeyRing + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.KeyRing{ + Id: testKeyRingID, + DisplayName: "Test Key Ring", + CreatedAt: testTime, + Description: utils.Ptr("This is a test key ring."), + State: kms.KEYRINGSTATE_ACTIVE, + }, + expected: fmt.Sprintf(` + ID │ %-37s +──────────────┼────────────────────────────────────── + DISPLAY NAME │ Test Key Ring +──────────────┼────────────────────────────────────── + CREATED AT │ %-37s +──────────────┼────────────────────────────────────── + STATE │ active +──────────────┼────────────────────────────────────── + DESCRIPTION │ This is a test key ring. + +`, + testKeyRingID, + testTime, + ), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(params.Out.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/kms/keyring/keyring.go b/internal/cmd/kms/keyring/keyring.go new file mode 100644 index 000000000..237991e5c --- /dev/null +++ b/internal/cmd/kms/keyring/keyring.go @@ -0,0 +1,32 @@ +package keyring + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/keyring/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/keyring/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/keyring/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/keyring/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "keyring", + Short: "Manage KMS key rings", + Long: "Provides functionality for key ring operations inside the KMS", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/kms/keyring/list/list.go b/internal/cmd/kms/keyring/list/list.go new file mode 100644 index 000000000..c7273cca9 --- /dev/null +++ b/internal/cmd/kms/keyring/list/list.go @@ -0,0 +1,116 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all KMS key rings", + Long: "Lists all KMS key rings.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all KMS key rings`, + "$ stackit kms keyring list"), + examples.NewExample( + `List all KMS key rings in JSON format`, + "$ stackit kms keyring list --output-format json"), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get KMS key rings: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.ProjectId, resp) + }, + } + + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListKeyRingsRequest { + req := apiClient.DefaultAPI.ListKeyRings(ctx, model.ProjectId, model.Region) + return req +} + +func outputResult(p *print.Printer, outputFormat, projectId string, resp *kms.KeyRingList) error { + if resp == nil || resp.KeyRings == nil { + return fmt.Errorf("response was nil / empty") + } + + keyRings := resp.KeyRings + + return p.OutputResult(outputFormat, keyRings, func() error { + if len(keyRings) == 0 { + p.Outputf("No key rings found for project %q\n", projectId) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "STATUS") + + for i := range keyRings { + keyRing := keyRings[i] + table.AddRow( + keyRing.Id, + keyRing.DisplayName, + keyRing.State, + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/kms/keyring/list/list_test.go b/internal/cmd/kms/keyring/list/list_test.go new file mode 100644 index 000000000..2a850b1d8 --- /dev/null +++ b/internal/cmd/kms/keyring/list/list_test.go @@ -0,0 +1,230 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiListKeyRingsRequest)) kms.ApiListKeyRingsRequest { + request := testClient.DefaultAPI.ListKeyRings(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiListKeyRingsRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + projectId string + resp *kms.KeyRingList + outputFormat string + projectLabel string + wantErr bool + }{ + { + description: "nil response", + resp: nil, + projectId: uuid.NewString(), + projectLabel: "my-project", + wantErr: true, + }, + { + description: "empty response", + resp: &kms.KeyRingList{}, + projectId: uuid.NewString(), + projectLabel: "my-project", + wantErr: true, + }, + { + description: "default output", + projectId: uuid.NewString(), + resp: &kms.KeyRingList{KeyRings: []kms.KeyRing{}}, + projectLabel: "my-project", + wantErr: false, + }, + { + description: "json output", + projectId: uuid.NewString(), + resp: &kms.KeyRingList{KeyRings: []kms.KeyRing{}}, + outputFormat: print.JSONOutputFormat, + wantErr: false, + }, + { + description: "yaml output", + projectId: uuid.NewString(), + resp: &kms.KeyRingList{KeyRings: []kms.KeyRing{}}, + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.projectId, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/kms.go b/internal/cmd/kms/kms.go new file mode 100644 index 000000000..46dc38c0b --- /dev/null +++ b/internal/cmd/kms/kms.go @@ -0,0 +1,32 @@ +package kms + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/key" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/keyring" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/version" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/wrappingkey" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "kms", + Short: "Provides functionality for KMS", + Long: "Provides functionality for KMS.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(keyring.NewCmd(params)) + cmd.AddCommand(wrappingkey.NewCmd(params)) + cmd.AddCommand(key.NewCmd(params)) + cmd.AddCommand(version.NewCmd(params)) +} diff --git a/internal/cmd/kms/version/destroy/destroy.go b/internal/cmd/kms/version/destroy/destroy.go new file mode 100644 index 000000000..04a418c61 --- /dev/null +++ b/internal/cmd/kms/version/destroy/destroy.go @@ -0,0 +1,130 @@ +package destroy + +import ( + "context" + "fmt" + "strconv" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" +) + +const ( + versionNumberArg = "VERSION_NUMBER" + + keyRingIdFlag = "keyring-id" + keyIdFlag = "key-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + KeyId string + VersionNumber int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("destroy %s", versionNumberArg), + Short: "Destroy a key version", + Long: "Removes the key material of a version.", + Args: args.SingleArg(versionNumberArg, nil), + Example: examples.Build( + examples.NewExample( + `Destroy key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`, + `$ stackit kms version destroy 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // This operation can be undone. Don't ask for confirmation! + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("destroy key Version: %w", err) + } + + // Get the key version in its state afterwards + resp, err := apiClient.DefaultAPI.GetVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key version: %v", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + versionStr := inputArgs[0] + versionNumber, err := strconv.ParseInt(versionStr, 10, 64) + if err != nil || versionNumber < 0 { + return nil, &errors.ArgValidationError{ + Arg: versionNumberArg, + Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr), + } + } + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag), + VersionNumber: versionNumber, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDestroyVersionRequest { + return apiClient.DefaultAPI.DestroyVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Destroyed version %d of the key %q\n", resp.Number, resp.KeyId) + return nil + }) +} diff --git a/internal/cmd/kms/version/destroy/destroy_test.go b/internal/cmd/kms/version/destroy/destroy_test.go new file mode 100644 index 000000000..2c4b5e5f9 --- /dev/null +++ b/internal/cmd/kms/version/destroy/destroy_test.go @@ -0,0 +1,321 @@ +package destroy + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" + testVersionNumber = int64(1) + testVersionNumberString = "1" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testVersionNumberString, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + keyIdFlag: testKeyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + VersionNumber: testVersionNumber, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiDestroyVersionRequest)) kms.ApiDestroyVersionRequest { + request := testClient.DefaultAPI.DestroyVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (versionNumber)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyIdFlag) + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "" + }), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "version number invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "version number invalid 2", + argValues: []string{"Not a Number!"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiDestroyVersionRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + wantErr bool + outputFormat string + resp *kms.Version + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "json output", + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "yaml output", + resp: &kms.Version{}, + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/version/disable/disable.go b/internal/cmd/kms/version/disable/disable.go new file mode 100644 index 000000000..85f2ee293 --- /dev/null +++ b/internal/cmd/kms/version/disable/disable.go @@ -0,0 +1,147 @@ +package disable + +import ( + "context" + "fmt" + "strconv" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" +) + +const ( + versionNumberArg = "VERSION_NUMBER" + + keyRingIdFlag = "keyring-id" + keyIdFlag = "key-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + KeyId string + VersionNumber int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("disable %s", versionNumberArg), + Short: "Disable a key version", + Long: "Disable the given key version.", + Args: args.SingleArg(versionNumberArg, nil), + Example: examples.Build( + examples.NewExample( + `Disable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`, + `$ stackit kms version disable 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // This operation can be undone. Don't ask for confirmation! + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("disable key version: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Disabling key version", func() error { + _, err = wait.DisableKeyVersionWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for key version to be disabled: %w", err) + } + } + + // Get the key version in its state afterwards + resp, err := apiClient.DefaultAPI.GetVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key version: %v", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + versionStr := inputArgs[0] + versionNumber, err := strconv.ParseInt(versionStr, 10, 64) + if err != nil || versionNumber < 0 { + return nil, &errors.ArgValidationError{ + Arg: versionNumberArg, + Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr), + } + } + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag), + VersionNumber: versionNumber, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDisableVersionRequest { + return apiClient.DefaultAPI.DisableVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, async bool, resp *kms.Version) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + operationState := "Disabled" + if async { + operationState = "Triggered disable of" + } + p.Outputf("%s version %d of the key %q\n", operationState, resp.Number, resp.KeyId) + return nil + }) +} diff --git a/internal/cmd/kms/version/disable/disable_test.go b/internal/cmd/kms/version/disable/disable_test.go new file mode 100644 index 000000000..86903bfc5 --- /dev/null +++ b/internal/cmd/kms/version/disable/disable_test.go @@ -0,0 +1,323 @@ +package disable + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" + testVersionNumber = int64(1) + testVersionNumberString = "1" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testVersionNumberString, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + keyIdFlag: testKeyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + VersionNumber: testVersionNumber, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiDisableVersionRequest)) kms.ApiDisableVersionRequest { + request := testClient.DefaultAPI.DisableVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (versionNumber)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyIdFlag) + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "" + }), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "version number invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "version number invalid 2", + argValues: []string{"Not a Number!"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiDisableVersionRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + wantErr bool + outputFormat string + async bool + resp *kms.Version + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "json output", + outputFormat: print.JSONOutputFormat, + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "yaml output", + outputFormat: print.YAMLOutputFormat, + resp: &kms.Version{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.async, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/version/enable/enable.go b/internal/cmd/kms/version/enable/enable.go new file mode 100644 index 000000000..7c7ff24c5 --- /dev/null +++ b/internal/cmd/kms/version/enable/enable.go @@ -0,0 +1,147 @@ +package enable + +import ( + "context" + "fmt" + "strconv" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" +) + +const ( + versionNumberArg = "VERSION_NUMBER" + + keyRingIdFlag = "keyring-id" + keyIdFlag = "key-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + KeyId string + VersionNumber int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("enable %s", versionNumberArg), + Short: "Enable a key version", + Long: "Enable the given key version.", + Args: args.SingleArg(versionNumberArg, nil), + Example: examples.Build( + examples.NewExample( + `Enable key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`, + `$ stackit kms version enable 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // This operation can be undone. Don't ask for confirmation! + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("enable key version: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Enabling key version", func() error { + _, err = wait.EnableKeyVersionWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for key version to be enabled: %w", err) + } + } + + // Get the key version in its state afterwards + resp, err := apiClient.DefaultAPI.GetVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key version: %v", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + versionStr := inputArgs[0] + versionNumber, err := strconv.ParseInt(versionStr, 10, 64) + if err != nil || versionNumber < 0 { + return nil, &errors.ArgValidationError{ + Arg: versionNumberArg, + Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr), + } + } + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag), + VersionNumber: versionNumber, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiEnableVersionRequest { + return apiClient.DefaultAPI.EnableVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, async bool, resp *kms.Version) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(outputFormat, resp, func() error { + operationState := "Enabled" + if async { + operationState = "Triggered enable of" + } + p.Outputf("%s version %d of the key %q\n", operationState, resp.Number, resp.KeyId) + return nil + }) +} diff --git a/internal/cmd/kms/version/enable/enable_test.go b/internal/cmd/kms/version/enable/enable_test.go new file mode 100644 index 000000000..7617208d8 --- /dev/null +++ b/internal/cmd/kms/version/enable/enable_test.go @@ -0,0 +1,323 @@ +package enable + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" + testVersionNumber = int64(1) + testVersionNumberString = "1" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testVersionNumberString, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + keyIdFlag: testKeyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + VersionNumber: testVersionNumber, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiEnableVersionRequest)) kms.ApiEnableVersionRequest { + request := testClient.DefaultAPI.EnableVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (versionNumber)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyIdFlag) + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "" + }), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "version number invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "version number invalid 2", + argValues: []string{"Not a Number!"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiEnableVersionRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + wantErr bool + outputFormat string + async bool + resp *kms.Version + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "json output", + resp: &kms.Version{}, + outputFormat: print.JSONOutputFormat, + wantErr: false, + }, + { + description: "yaml output", + resp: &kms.Version{}, + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.async, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/version/list/list.go b/internal/cmd/kms/version/list/list.go new file mode 100644 index 000000000..c1600d020 --- /dev/null +++ b/internal/cmd/kms/version/list/list.go @@ -0,0 +1,134 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + keyRingIdFlag = "keyring-id" + keyIdFlag = "key-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + KeyId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all key versions", + Long: "List all versions of a given key.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all key versions for the key "my-key-id" inside the key ring "my-keyring-id"`, + `$ stackit kms version list --key-id "my-key-id" --keyring-id "my-keyring-id"`), + examples.NewExample( + `List all key versions in JSON format`, + `$ stackit kms version list --key-id "my-key-id" --keyring-id "my-keyring-id" -o json`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get key version: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.ProjectId, model.KeyId, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListVersionsRequest { + return apiClient.DefaultAPI.ListVersions(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId) +} + +func outputResult(p *print.Printer, outputFormat, projectId, keyId string, resp *kms.VersionList) error { + if resp == nil || resp.Versions == nil { + return fmt.Errorf("response is nil / empty") + } + versions := resp.Versions + + return p.OutputResult(outputFormat, versions, func() error { + if len(versions) == 0 { + p.Outputf("No key versions found for project %q for the key %q\n", projectId, keyId) + return nil + } + table := tables.NewTable() + table.SetHeader("ID", "NUMBER", "CREATED AT", "DESTROY DATE", "STATUS") + + for _, version := range versions { + table.AddRow( + version.KeyId, + version.Number, + version.CreatedAt, + utils.PtrString(version.DestroyDate), + version.State, + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag) + cobra.CheckErr(err) +} diff --git a/internal/cmd/kms/version/list/list_test.go b/internal/cmd/kms/version/list/list_test.go new file mode 100644 index 000000000..479d5e19b --- /dev/null +++ b/internal/cmd/kms/version/list/list_test.go @@ -0,0 +1,283 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" +) + +const ( + testRegion = "eu02" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + keyIdFlag: testKeyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiListVersionsRequest)) kms.ApiListVersionsRequest { + request := testClient.DefaultAPI.ListVersions(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyIdFlag) + }), + isValid: false, + }, + { + description: "key id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "" + }), + isValid: false, + }, + { + description: "key id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiListVersionsRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + projectId string + keyId string + resp *kms.VersionList + outputFormat string + projectLabel string + wantErr bool + }{ + { + description: "nil response", + resp: nil, + projectLabel: "my-project", + wantErr: true, + }, + { + description: "empty default", + resp: &kms.VersionList{}, + projectLabel: "my-project", + wantErr: true, + }, + { + description: "default output", + resp: &kms.VersionList{Versions: []kms.Version{}}, + projectLabel: "my-project", + wantErr: false, + }, + { + description: "json output", + resp: &kms.VersionList{Versions: []kms.Version{}}, + outputFormat: print.JSONOutputFormat, + wantErr: false, + }, + { + description: "yaml output", + resp: &kms.VersionList{Versions: []kms.Version{}}, + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.projectId, tt.keyId, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/version/restore/restore.go b/internal/cmd/kms/version/restore/restore.go new file mode 100644 index 000000000..2aba9b04b --- /dev/null +++ b/internal/cmd/kms/version/restore/restore.go @@ -0,0 +1,130 @@ +package restore + +import ( + "context" + "fmt" + "strconv" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" +) + +const ( + versionNumberArg = "VERSION_NUMBER" + + keyRingIdFlag = "keyring-id" + keyIdFlag = "key-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + KeyId string + VersionNumber int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("restore %s", versionNumberArg), + Short: "Restore a key version", + Long: "Restores the specified version of a key.", + Args: args.SingleArg(versionNumberArg, nil), + Example: examples.Build( + examples.NewExample( + `Restore key version "42" for the key "my-key-id" inside the key ring "my-keyring-id"`, + `$ stackit kms version restore 42 --key-id "my-key-id" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // This operation can be undone. Don't ask for confirmation! + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("restore key Version: %w", err) + } + + // Grab the key after the restore was applied to display the new state to the user. + resp, err := apiClient.DefaultAPI.GetVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber).Execute() + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get key version: %v", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + versionStr := inputArgs[0] + versionNumber, err := strconv.ParseInt(versionStr, 10, 64) + if err != nil || versionNumber < 0 { + return nil, &errors.ArgValidationError{ + Arg: versionNumberArg, + Details: fmt.Sprintf("invalid value %q: must be a positive integer", versionStr), + } + } + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + KeyId: flags.FlagToStringValue(p, cmd, keyIdFlag), + VersionNumber: versionNumber, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiRestoreVersionRequest { + return apiClient.DefaultAPI.RestoreVersion(ctx, model.ProjectId, model.Region, model.KeyRingId, model.KeyId, model.VersionNumber) +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().Var(flags.UUIDFlag(), keyIdFlag, "ID of the key") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, keyIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat string, resp *kms.Version) error { + if resp == nil { + return fmt.Errorf("response is nil / empty") + } + + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Restored version %d of the key %q\n", resp.Number, resp.KeyId) + return nil + }) +} diff --git a/internal/cmd/kms/version/restore/restore_test.go b/internal/cmd/kms/version/restore/restore_test.go new file mode 100644 index 000000000..8268f1d56 --- /dev/null +++ b/internal/cmd/kms/version/restore/restore_test.go @@ -0,0 +1,322 @@ +package restore + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +const ( + testRegion = "eu02" + testVersionNumber = int64(1) + testVersionNumberString = "1" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testVersionNumberString, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + keyIdFlag: testKeyId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + KeyId: testKeyId, + VersionNumber: testVersionNumber, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiRestoreVersionRequest)) kms.ApiRestoreVersionRequest { + request := testClient.DefaultAPI.RestoreVersion(testCtx, testProjectId, testRegion, testKeyRingId, testKeyId, testVersionNumber) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (versionNumber)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "key ring id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyIdFlag) + }), + isValid: false, + }, + { + description: "key id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "" + }), + isValid: false, + }, + { + description: "key id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "version number invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "version number invalid 2", + argValues: []string{"Not a Number!"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiRestoreVersionRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + wantErr bool + outputFormat string + resp *kms.Version + }{ + { + description: "nil response", + resp: nil, + wantErr: true, + }, + { + description: "default output", + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "json output", + outputFormat: print.JSONOutputFormat, + resp: &kms.Version{}, + wantErr: false, + }, + { + description: "yaml output", + outputFormat: print.YAMLOutputFormat, + resp: &kms.Version{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/version/version.go b/internal/cmd/kms/version/version.go new file mode 100644 index 000000000..e1a7d56fe --- /dev/null +++ b/internal/cmd/kms/version/version.go @@ -0,0 +1,34 @@ +package version + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/version/destroy" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/version/disable" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/version/enable" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/version/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/version/restore" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "version", + Short: "Manage KMS key versions", + Long: "Provides functionality for key version operations inside the KMS", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(destroy.NewCmd(params)) + cmd.AddCommand(disable.NewCmd(params)) + cmd.AddCommand(enable.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(restore.NewCmd(params)) +} diff --git a/internal/cmd/kms/wrappingkey/create/create.go b/internal/cmd/kms/wrappingkey/create/create.go new file mode 100644 index 000000000..8d8295329 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/create/create.go @@ -0,0 +1,183 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + keyRingIdFlag = "keyring-id" + + descriptionFlag = "description" + displayNameFlag = "name" +) + +var ( + algorithmFlag = flags.StringEnumFlag( + "algorithm", + kms.AllowedWrappingAlgorithmEnumValues, + "En-/Decryption / signing algorithm.", + ) + purposeFlag = flags.StringEnumFlag( + "purpose", + kms.AllowedWrappingPurposeEnumValues, + "Purpose of the key.", + ) + protectionFlag = flags.StringEnumFlag( + "protection", + kms.AllowedProtectionEnumValues, + "The underlying system that is responsible for protecting the key material.") +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string + + Algorithm kms.WrappingAlgorithm + Description *string + Name *string + Purpose kms.WrappingPurpose + Protection kms.Protection +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a KMS wrapping key", + Long: "Creates a KMS wrapping key.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a symmetric (RSA + AES) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id"`, + `$ stackit kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_2048_oaep_sha256_aes_256_key_wrap" --name "my-wrapping-key-name" --purpose "wrap_symmetric_key" --protection "software"`), + examples.NewExample( + `Create an asymmetric (RSA) KMS wrapping key with name "my-wrapping-key-name" in key ring with ID "my-keyring-id"`, + `$ stackit kms wrapping-key create --keyring-id "my-keyring-id" --algorithm "rsa_3072_oaep_sha256" --name "my-wrapping-key-name" --purpose "wrap_asymmetric_key" --protection "software"`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + err = params.Printer.PromptForConfirmation("Are you sure you want to create a KMS wrapping key?") + if err != nil { + return err + } + + // Call API + req, _ := buildRequest(ctx, model, apiClient.DefaultAPI) + wrappingKey, err := req.Execute() + if err != nil { + return fmt.Errorf("create KMS wrapping key: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating wrapping key", func() error { + _, err = wait.CreateWrappingKeyWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, wrappingKey.KeyRingId, wrappingKey.Id).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for KMS wrapping key creation: %w", err) + } + } + + return outputResult(params.Printer, model, wrappingKey) + }, + } + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + // All values are mandatory strings. No additional type check required. + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + Algorithm: algorithmFlag.Get(), + Name: flags.FlagToStringPointer(p, cmd, displayNameFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Purpose: purposeFlag.Get(), + Protection: protectionFlag.Get(), + } + + p.DebugInputModel(model) + return &model, nil +} + +type kmsWrappingKeyClient interface { + CreateWrappingKey(ctx context.Context, projectId string, regionId string, keyRingId string) kms.ApiCreateWrappingKeyRequest +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient kmsWrappingKeyClient) (kms.ApiCreateWrappingKeyRequest, error) { + req := apiClient.CreateWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingId) + + req = req.CreateWrappingKeyPayload(kms.CreateWrappingKeyPayload{ + DisplayName: utils.PtrString(model.Name), + Description: model.Description, + Algorithm: model.Algorithm, + Purpose: model.Purpose, + Protection: model.Protection, + }) + return req, nil +} + +func outputResult(p *print.Printer, model *inputModel, resp *kms.WrappingKey) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s wrapping key. Wrapping key ID: %s\n", operationState, resp.Id) + return nil + }) +} + +func configureFlags(cmd *cobra.Command) { + algorithmFlag.Register(cmd.Flags()) + purposeFlag.Register(cmd.Flags()) + protectionFlag.Register(cmd.Flags()) + + // All further non Enum Flags + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring") + cmd.Flags().String(displayNameFlag, "", "The display name to distinguish multiple wrapping keys") + cmd.Flags().String(descriptionFlag, "", "Optional description of the wrapping key") + + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag, algorithmFlag.Name(), purposeFlag.Name(), displayNameFlag, protectionFlag.Name()) + cobra.CheckErr(err) +} diff --git a/internal/cmd/kms/wrappingkey/create/create_test.go b/internal/cmd/kms/wrappingkey/create/create_test.go new file mode 100644 index 000000000..981e60637 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/create/create_test.go @@ -0,0 +1,319 @@ +package create + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testRegion = "eu01" + testAlgorithm = kms.WRAPPINGALGORITHM_RSA_2048_OAEP_SHA256 + testDisplayName = "my-key" + testPurpose = kms.WRAPPINGPURPOSE_WRAP_ASYMMETRIC_KEY + testDescription = "my key description" + testProtection = kms.PROTECTION_SOFTWARE +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + algorithmFlag.Name(): string(testAlgorithm), + displayNameFlag: testDisplayName, + purposeFlag.Name(): string(testPurpose), + descriptionFlag: testDescription, + protectionFlag.Name(): string(testProtection), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + Algorithm: testAlgorithm, + Name: utils.Ptr(testDisplayName), + Purpose: testPurpose, + Description: utils.Ptr(testDescription), + Protection: testProtection, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiCreateWrappingKeyRequest)) kms.ApiCreateWrappingKeyRequest { + request := testClient.DefaultAPI.CreateWrappingKey(testCtx, testProjectId, testRegion, testKeyRingId) + request = request.CreateWrappingKeyPayload(kms.CreateWrappingKeyPayload{ + Algorithm: testAlgorithm, + DisplayName: testDisplayName, + Purpose: testPurpose, + Description: utils.Ptr(testDescription), + Protection: testProtection, + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "optional flags omitted", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "algorithm missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, algorithmFlag.Name()) + }), + isValid: false, + }, + { + description: "name missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + { + description: "purpose missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, purposeFlag.Name()) + }), + isValid: false, + }, + { + description: "protection missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, protectionFlag.Name()) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiCreateWrappingKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + expectedRequest: fixtureRequest().CreateWrappingKeyPayload(kms.CreateWrappingKeyPayload{ + Algorithm: testAlgorithm, + DisplayName: testDisplayName, + Purpose: testPurpose, + Protection: testProtection, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI) + if err != nil { + t.Fatalf("error building request: %v", err) + } + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + wrappingKey *kms.WrappingKey + wantErr bool + }{ + { + description: "nil response", + wrappingKey: nil, + wantErr: true, + }, + { + description: "default output", + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wrappingKey: &kms.WrappingKey{}, + wantErr: false, + }, + { + description: "json output", + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wrappingKey: &kms.WrappingKey{}, + wantErr: false, + }, + { + description: "yaml output", + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wrappingKey: &kms.WrappingKey{}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, tt.wrappingKey) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/wrappingkey/delete/delete.go b/internal/cmd/kms/wrappingkey/delete/delete.go new file mode 100644 index 000000000..d51d6fab8 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/delete/delete.go @@ -0,0 +1,119 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + kmsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" +) + +const ( + wrappingKeyIdArg = "WRAPPING_KEY_ID" + + keyRingIdFlag = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + WrappingKeyId string + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", wrappingKeyIdArg), + Short: "Deletes a KMS wrapping key", + Long: "Deletes a KMS wrapping key inside a specific key ring.", + Args: args.SingleArg(wrappingKeyIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a KMS wrapping key "MY_WRAPPING_KEY_ID" inside the key ring "my-keyring-id"`, + `$ stackit kms wrapping-key delete "MY_WRAPPING_KEY_ID" --keyring-id "my-keyring-id"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + wrappingKeyName, err := kmsUtils.GetWrappingKeyName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.KeyRingId, model.WrappingKeyId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get wrapping key name: %v", err) + wrappingKeyName = model.WrappingKeyId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the wrapping key %q? (this cannot be undone)", wrappingKeyName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete KMS wrapping key: %w", err) + } + + // Wait for async operation not relevant. Wrapping key deletion is synchronous + + // Don't output anything. It's a deletion. + params.Printer.Info("Deleted wrapping key %q\n", wrappingKeyName) + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + wrappingKeyId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + WrappingKeyId: wrappingKeyId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiDeleteWrappingKeyRequest { + req := apiClient.DefaultAPI.DeleteWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingId, model.WrappingKeyId) + return req +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the wrapping key is stored") + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag) + cobra.CheckErr(err) +} diff --git a/internal/cmd/kms/wrappingkey/delete/delete_test.go b/internal/cmd/kms/wrappingkey/delete/delete_test.go new file mode 100644 index 000000000..225a73c06 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/delete/delete_test.go @@ -0,0 +1,242 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testWrappingKeyId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testWrappingKeyId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + WrappingKeyId: testWrappingKeyId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiDeleteWrappingKeyRequest)) kms.ApiDeleteWrappingKeyRequest { + request := testClient.DefaultAPI.DeleteWrappingKey(testCtx, testProjectId, testRegion, testKeyRingId, testWrappingKeyId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no args (wrappingKeyId)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values provided", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "key ring id missing (required)", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "key ring id invalid", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "wrapping key id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "wrapping key id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiDeleteWrappingKeyRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/kms/wrappingkey/describe/describe.go b/internal/cmd/kms/wrappingkey/describe/describe.go new file mode 100644 index 000000000..861341140 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/describe/describe.go @@ -0,0 +1,133 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + argWrappingKeyID = "WRAPPING_KEY_ID" + flagKeyRingID = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + WrappingKeyID string + KeyRingID string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argWrappingKeyID), + Short: "Describe a KMS wrapping key", + Long: "Describe a KMS wrapping key", + Args: args.SingleArg(argWrappingKeyID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a KMS wrapping key with ID xxx of keyring yyy`, + `$ stackit kms wrapping-key describe xxx --keyring-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get wrapping key: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), flagKeyRingID, "Key Ring ID") + err := flags.MarkFlagsRequired(cmd, flagKeyRingID) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + WrappingKeyID: inputArgs[0], + KeyRingID: flags.FlagToStringValue(p, cmd, flagKeyRingID), + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiGetWrappingKeyRequest { + return apiClient.DefaultAPI.GetWrappingKey(ctx, model.ProjectId, model.Region, model.KeyRingID, model.WrappingKeyID) +} + +func outputResult(p *print.Printer, outputFormat string, wrappingKey *kms.WrappingKey) error { + if wrappingKey == nil { + return fmt.Errorf("wrapping key response is empty") + } + return p.OutputResult(outputFormat, wrappingKey, func() error { + table := tables.NewTable() + table.AddRow("ID", wrappingKey.Id) + table.AddSeparator() + table.AddRow("DISPLAY NAME", wrappingKey.DisplayName) + table.AddSeparator() + table.AddRow("CREATED AT", wrappingKey.CreatedAt) + table.AddSeparator() + table.AddRow("STATE", wrappingKey.State) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(wrappingKey.Description)) + table.AddSeparator() + table.AddRow("ACCESS SCOPE", wrappingKey.AccessScope) + table.AddSeparator() + table.AddRow("ALGORITHM", wrappingKey.Algorithm) + table.AddSeparator() + table.AddRow("EXPIRES AT", wrappingKey.ExpiresAt) + table.AddSeparator() + table.AddRow("KEYRING ID", wrappingKey.KeyRingId) + table.AddSeparator() + table.AddRow("PROTECTION", wrappingKey.Protection) + table.AddSeparator() + table.AddRow("PUBLIC KEY", utils.PtrString(wrappingKey.PublicKey)) + table.AddSeparator() + table.AddRow("PURPOSE", wrappingKey.Purpose) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/kms/wrappingkey/describe/describe_test.go b/internal/cmd/kms/wrappingkey/describe/describe_test.go new file mode 100644 index 000000000..b34d292f4 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/describe/describe_test.go @@ -0,0 +1,212 @@ +package describe + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testKeyRingID = uuid.NewString() +var testWrappingKeyID = uuid.NewString() +var testTime = time.Time{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + flagKeyRingID: testKeyRingID, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingID: testKeyRingID, + WrappingKeyID: testWrappingKeyID, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: []string{testWrappingKeyID}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "invalid key ring id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[flagKeyRingID] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing project id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "invalid project id", + argValues: []string{testWrappingKeyID}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + got := buildRequest(testCtx, fixtureInputModel(), testClient) + want := testClient.DefaultAPI.GetWrappingKey(testCtx, testProjectId, testRegion, testKeyRingID, testWrappingKeyID) + diff := cmp.Diff(got, want, + cmp.AllowUnexported(want), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("buildRequest() mismatch (-want +got):\n%s", diff) + } +} +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + outputFmt string + keyRing *kms.WrappingKey + wantErr bool + expected string + }{ + { + description: "empty", + outputFmt: "table", + wantErr: true, + }, + { + description: "table format", + outputFmt: "table", + keyRing: &kms.WrappingKey{ + Id: testWrappingKeyID, + DisplayName: "Test Key Ring", + CreatedAt: testTime, + Description: utils.Ptr("This is a test key ring."), + State: kms.WRAPPINGKEYSTATE_ACTIVE, + AccessScope: kms.ACCESSSCOPE_PUBLIC, + Algorithm: kms.WRAPPINGALGORITHM_RSA_2048_OAEP_SHA256, + ExpiresAt: testTime, + KeyRingId: testKeyRingID, + Protection: kms.PROTECTION_SOFTWARE, + PublicKey: utils.Ptr("-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ...\n-----END PUBLIC KEY-----"), + Purpose: kms.WRAPPINGPURPOSE_WRAP_ASYMMETRIC_KEY, + }, + expected: fmt.Sprintf(` + ID │ %-46s +──────────────┼─────────────────────────────────────────────── + DISPLAY NAME │ Test Key Ring +──────────────┼─────────────────────────────────────────────── + CREATED AT │ %-46s +──────────────┼─────────────────────────────────────────────── + STATE │ active +──────────────┼─────────────────────────────────────────────── + DESCRIPTION │ This is a test key ring. +──────────────┼─────────────────────────────────────────────── + ACCESS SCOPE │ PUBLIC +──────────────┼─────────────────────────────────────────────── + ALGORITHM │ rsa_2048_oaep_sha256 +──────────────┼─────────────────────────────────────────────── + EXPIRES AT │ %-46s +──────────────┼─────────────────────────────────────────────── + KEYRING ID │ %-46s +──────────────┼─────────────────────────────────────────────── + PROTECTION │ software +──────────────┼─────────────────────────────────────────────── + PUBLIC KEY │ -----BEGIN PUBLIC KEY----- + │ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQ... + │ -----END PUBLIC KEY----- +──────────────┼─────────────────────────────────────────────── + PURPOSE │ wrap_asymmetric_key + +`, + testWrappingKeyID, + testTime, + testTime, + testKeyRingID), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + if err := outputResult(params.Printer, tt.outputFmt, tt.keyRing); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + diff := cmp.Diff(params.Out.String(), tt.expected) + if diff != "" { + t.Fatalf("outputResult() output mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/kms/wrappingkey/list/list.go b/internal/cmd/kms/wrappingkey/list/list.go new file mode 100644 index 000000000..06b6ea4b7 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/list/list.go @@ -0,0 +1,132 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/kms/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + keyRingIdFlag = "keyring-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + KeyRingId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all KMS wrapping keys", + Long: "Lists all KMS wrapping keys inside a key ring.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all KMS wrapping keys for the key ring "my-keyring-id"`, + `$ stackit kms wrapping-key list --keyring-id "my-keyring-id"`), + examples.NewExample( + `List all KMS wrapping keys in JSON format`, + `$ stackit kms wrapping-key list --keyring-id "my-keyring-id" --output-format json`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get KMS wrapping keys: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, model.KeyRingId, resp) + }, + } + + configureFlags(cmd) + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + KeyRingId: flags.FlagToStringValue(p, cmd, keyRingIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *kms.APIClient) kms.ApiListWrappingKeysRequest { + req := apiClient.DefaultAPI.ListWrappingKeys(ctx, model.ProjectId, model.Region, model.KeyRingId) + return req +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), keyRingIdFlag, "ID of the KMS key ring where the key is stored") + err := flags.MarkFlagsRequired(cmd, keyRingIdFlag) + cobra.CheckErr(err) +} + +func outputResult(p *print.Printer, outputFormat, keyRingId string, resp *kms.WrappingKeyList) error { + if resp == nil || resp.WrappingKeys == nil { + return fmt.Errorf("response is nil / empty") + } + + wrappingKeys := resp.WrappingKeys + + return p.OutputResult(outputFormat, wrappingKeys, func() error { + if len(wrappingKeys) == 0 { + p.Outputf("No wrapping keys found under the key ring %q\n", keyRingId) + return nil + } + table := tables.NewTable() + table.SetHeader("ID", "NAME", "SCOPE", "ALGORITHM", "EXPIRES AT", "STATUS") + + for i := range wrappingKeys { + wrappingKey := wrappingKeys[i] + table.AddRow( + wrappingKey.Id, + wrappingKey.DisplayName, + wrappingKey.Purpose, + wrappingKey.Algorithm, + wrappingKey.ExpiresAt, + wrappingKey.State, + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/kms/wrappingkey/list/list_test.go b/internal/cmd/kms/wrappingkey/list/list_test.go new file mode 100644 index 000000000..d13f01ca7 --- /dev/null +++ b/internal/cmd/kms/wrappingkey/list/list_test.go @@ -0,0 +1,251 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" +) + +const ( + testRegion = "eu02" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &kms.APIClient{DefaultAPI: &kms.DefaultAPIService{}} + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + keyRingIdFlag: testKeyRingId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + KeyRingId: testKeyRingId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *kms.ApiListWrappingKeysRequest)) kms.ApiListWrappingKeysRequest { + request := testClient.DefaultAPI.ListWrappingKeys(testCtx, testProjectId, testRegion, testKeyRingId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing keyRingId", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, keyRingIdFlag) + }), + isValid: false, + }, + { + description: "invalid keyRingId 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid keyRingId 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[keyRingIdFlag] = "Not an uuid" + }), + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + params := testparams.NewTestParams() + model, err := parseInput(params.Printer, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(tt.expectedModel, model) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest kms.ApiListWrappingKeysRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, kms.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + keyRingId string + resp *kms.WrappingKeyList + outputFormat string + projectLabel string + wantErr bool + }{ + { + description: "nil response", + resp: nil, + projectLabel: "my-project", + wantErr: true, + }, + { + description: "default output", + resp: &kms.WrappingKeyList{WrappingKeys: []kms.WrappingKey{}}, + projectLabel: "my-project", + wantErr: false, + }, + { + description: "json output", + resp: &kms.WrappingKeyList{WrappingKeys: []kms.WrappingKey{}}, + outputFormat: print.JSONOutputFormat, + wantErr: false, + }, + { + description: "yaml output", + resp: &kms.WrappingKeyList{WrappingKeys: []kms.WrappingKey{}}, + outputFormat: print.YAMLOutputFormat, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.outputFormat, tt.keyRingId, tt.resp) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/kms/wrappingkey/wrappingkey.go b/internal/cmd/kms/wrappingkey/wrappingkey.go new file mode 100644 index 000000000..ab873566d --- /dev/null +++ b/internal/cmd/kms/wrappingkey/wrappingkey.go @@ -0,0 +1,32 @@ +package wrappingkey + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/wrappingkey/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/wrappingkey/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/wrappingkey/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms/wrappingkey/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "wrapping-key", + Short: "Manage KMS wrapping keys", + Long: "Provides functionality for wrapping key operations inside the KMS", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/load-balancer/create/create.go b/internal/cmd/load-balancer/create/create.go index f59683a47..bf44700e7 100644 --- a/internal/cmd/load-balancer/create/create.go +++ b/internal/cmd/load-balancer/create/create.go @@ -5,9 +5,13 @@ import ( "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/google/uuid" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,8 +22,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" ) const ( @@ -35,14 +37,14 @@ var ( xRequestId = uuid.NewString() ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Load Balancer", Long: fmt.Sprintf("%s\n%s\n%s", "Creates a Load Balancer.", "The payload can be provided as a JSON string or a file path prefixed with \"@\".", - "See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.", + "See https://docs.api.stackit.cloud/documentation/load-balancer/version/v2#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.", ), Args: args.NoArgs, Example: examples.Build( @@ -58,9 +60,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { ``, `$ stackit load-balancer create --payload @./payload.json`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -77,12 +79,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a load balancer for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a load balancer for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -94,13 +94,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating load balancer") - _, err = wait.CreateLoadBalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, *model.Payload.Name).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating load balancer", func() error { + _, err = wait.CreateLoadBalancerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.Payload.Name).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for load balancer creation: %w", err) } - s.Stop() } operationState := "Created" @@ -122,7 +122,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -143,20 +143,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Payload: payload, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiCreateLoadBalancerRequest { - req := apiClient.CreateLoadBalancer(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.CreateLoadBalancer(ctx, model.ProjectId, model.Region) req = req.CreateLoadBalancerPayload(*model.Payload) req = req.XRequestID(xRequestId) return req diff --git a/internal/cmd/load-balancer/create/create_test.go b/internal/cmd/load-balancer/create/create_test.go index 66cdb2754..ed54198d1 100644 --- a/internal/cmd/load-balancer/create/create_test.go +++ b/internal/cmd/load-balancer/create/create_test.go @@ -4,11 +4,10 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -24,7 +23,7 @@ type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &loadbalancer.APIClient{} + testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} testProjectId = uuid.NewString() testRequestId = xRequestId ) @@ -32,12 +31,12 @@ var ( var testPayload = &loadbalancer.CreateLoadBalancerPayload{ ExternalAddress: utils.Ptr(""), - Listeners: &[]loadbalancer.Listener{ + Listeners: []loadbalancer.Listener{ { DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), - Protocol: loadbalancer.ListenerProtocol("").Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + Port: utils.Ptr(int32(0)), + Protocol: loadbalancer.ListenerProtocol("unknown_default_open_api").Ptr(), + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -52,15 +51,15 @@ var testPayload = &loadbalancer.CreateLoadBalancerPayload{ }, }, Name: utils.Ptr(""), - Networks: &[]loadbalancer.Network{ + Networks: []loadbalancer.Network{ { NetworkId: utils.Ptr(""), - Role: loadbalancer.NetworkRole("").Ptr(), + Role: loadbalancer.NetworkRole("unknown_default_open_api").Ptr(), }, }, Options: &loadbalancer.LoadBalancerOptions{ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: &[]string{ + AllowedSourceRanges: []string{ "", }, }, @@ -77,21 +76,21 @@ var testPayload = &loadbalancer.CreateLoadBalancerPayload{ }, PrivateNetworkOnly: utils.Ptr(false), }, - TargetPools: &[]loadbalancer.TargetPool{ + TargetPools: []loadbalancer.TargetPool{ { ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(0)), + HealthyThreshold: utils.Ptr(int32(0)), Interval: utils.Ptr(""), IntervalJitter: utils.Ptr(""), Timeout: utils.Ptr(""), - UnhealthyThreshold: utils.Ptr(int64(0)), + UnhealthyThreshold: utils.Ptr(int32(0)), }, Name: utils.Ptr(""), SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(false), }, - TargetPort: utils.Ptr(int64(0)), - Targets: &[]loadbalancer.Target{ + TargetPort: utils.Ptr(int32(0)), + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr(""), Ip: utils.Ptr(""), @@ -198,7 +197,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiCreateLoadBalancerRequest)) loadbalancer.ApiCreateLoadBalancerRequest { - request := testClient.CreateLoadBalancer(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateLoadBalancer(testCtx, testProjectId, testRegion) request = request.CreateLoadBalancerPayload(*testPayload) request = request.XRequestID(testRequestId) for _, mod := range mods { @@ -210,6 +209,7 @@ func fixtureRequest(mods ...func(request *loadbalancer.ApiCreateLoadBalancerRequ func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -277,56 +277,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(*model, *tt.expectedModel, - cmpopts.EquateComparable(testCtx), - ) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithOptions(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, nil, tt.isValid, []testutils.TestingOption{ + testutils.WithCmpOptions(cmpopts.EquateEmpty()), + }) }) } } @@ -351,7 +304,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/delete/delete.go b/internal/cmd/load-balancer/delete/delete.go index 84146f875..efc79b0fd 100644 --- a/internal/cmd/load-balancer/delete/delete.go +++ b/internal/cmd/load-balancer/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api/wait" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { LoadBalancerName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", loadBalancerNameArg), Short: "Deletes a Load Balancer", @@ -50,12 +51,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete load balancer %q? (This cannot be undone)", model.LoadBalancerName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete load balancer %q? (This cannot be undone)", model.LoadBalancerName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -67,13 +66,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting load balancer") - _, err = wait.DeleteLoadBalancerWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.LoadBalancerName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting load balancer", func() error { + _, err = wait.DeleteLoadBalancerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.LoadBalancerName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for load balancer deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -100,19 +99,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu LoadBalancerName: loadBalancerName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiDeleteLoadBalancerRequest { - req := apiClient.DeleteLoadBalancer(ctx, model.ProjectId, model.Region, model.LoadBalancerName) + req := apiClient.DefaultAPI.DeleteLoadBalancer(ctx, model.ProjectId, model.Region, model.LoadBalancerName) return req } diff --git a/internal/cmd/load-balancer/delete/delete_test.go b/internal/cmd/load-balancer/delete/delete_test.go index 17cca82cb..8842ca890 100644 --- a/internal/cmd/load-balancer/delete/delete_test.go +++ b/internal/cmd/load-balancer/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -21,7 +20,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() var testLoadBalancerName = "loadBalancer" @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiDeleteLoadBalancerRequest)) loadbalancer.ApiDeleteLoadBalancerRequest { - request := testClient.DeleteLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) + request := testClient.DefaultAPI.DeleteLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) for _, mod := range mods { mod(&request) } @@ -130,54 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/describe/describe.go b/internal/cmd/load-balancer/describe/describe.go index 2b5ff659d..a4fb843cc 100644 --- a/internal/cmd/load-balancer/describe/describe.go +++ b/internal/cmd/load-balancer/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { LoadBalancerName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", loadBalancerNameArg), Short: "Shows details of a Load Balancer", @@ -82,20 +81,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu LoadBalancerName: loadBalancerName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiGetLoadBalancerRequest { - req := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.LoadBalancerName) + req := apiClient.DefaultAPI.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.LoadBalancerName) return req } @@ -103,47 +94,25 @@ func outputResult(p *print.Printer, outputFormat string, loadBalancer *loadbalan if loadBalancer == nil { return fmt.Errorf("loadbalancer response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(loadBalancer, "", " ") - if err != nil { - return fmt.Errorf("marshal load balancer: %w", err) + + return p.OutputResult(outputFormat, loadBalancer, func() error { + content := []tables.Table{} + content = append(content, buildLoadBalancerTable(loadBalancer)) + + if loadBalancer.Listeners != nil { + content = append(content, buildListenersTable(loadBalancer.Listeners)) + } + if loadBalancer.TargetPools != nil { + content = append(content, buildTargetPoolsTable(loadBalancer.TargetPools)) } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(loadBalancer, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + err := tables.DisplayTables(p, content) if err != nil { - return fmt.Errorf("marshal load balancer: %w", err) + return fmt.Errorf("display output: %w", err) } - p.Outputln(string(details)) return nil - default: - return outputResultAsTable(p, loadBalancer) - } -} - -func outputResultAsTable(p *print.Printer, loadBalancer *loadbalancer.LoadBalancer) error { - content := []tables.Table{} - - content = append(content, buildLoadBalancerTable(loadBalancer)) - - if loadBalancer.Listeners != nil { - content = append(content, buildListenersTable(*loadBalancer.Listeners)) - } - - if loadBalancer.TargetPools != nil { - content = append(content, buildTargetPoolsTable(*loadBalancer.TargetPools)) - } - - err := tables.DisplayTables(p, content) - if err != nil { - return fmt.Errorf("display output: %w", err) - } - - return nil + }) } func buildLoadBalancerTable(loadBalancer *loadbalancer.LoadBalancer) tables.Table { @@ -151,7 +120,7 @@ func buildLoadBalancerTable(loadBalancer *loadbalancer.LoadBalancer) tables.Tabl privateAccessOnly := false if loadBalancer.Options != nil { if loadBalancer.Options.AccessControl != nil && loadBalancer.Options.AccessControl.AllowedSourceRanges != nil { - acl = *loadBalancer.Options.AccessControl.AllowedSourceRanges + acl = loadBalancer.Options.AccessControl.AllowedSourceRanges } if loadBalancer.Options.PrivateNetworkOnly != nil { @@ -160,16 +129,16 @@ func buildLoadBalancerTable(loadBalancer *loadbalancer.LoadBalancer) tables.Tabl } networkId := "-" - if loadBalancer.Networks != nil && len(*loadBalancer.Networks) > 0 { - networks := *loadBalancer.Networks + if len(loadBalancer.Networks) > 0 { + networks := loadBalancer.Networks networkId = *networks[0].NetworkId } externalAddress := utils.PtrStringDefault(loadBalancer.ExternalAddress, "-") - errorDescriptions := []string{} - if loadBalancer.Errors != nil && len((*loadBalancer.Errors)) > 0 { - for _, err := range *loadBalancer.Errors { + var errorDescriptions []string + if len(loadBalancer.Errors) > 0 { + for _, err := range loadBalancer.Errors { errorDescriptions = append(errorDescriptions, *err.Description) } } @@ -215,7 +184,7 @@ func buildTargetPoolsTable(targetPools []loadbalancer.TargetPool) tables.Table { table.SetTitle("Target Pools") table.SetHeader("NAME", "PORT", "TARGETS") for _, targetPool := range targetPools { - table.AddRow(utils.PtrString(targetPool.Name), utils.PtrString(targetPool.TargetPort), len(*targetPool.Targets)) + table.AddRow(utils.PtrString(targetPool.Name), utils.PtrString(targetPool.TargetPort), len(targetPool.Targets)) } return table } diff --git a/internal/cmd/load-balancer/describe/describe_test.go b/internal/cmd/load-balancer/describe/describe_test.go index 2aebb5e61..9547ed15e 100644 --- a/internal/cmd/load-balancer/describe/describe_test.go +++ b/internal/cmd/load-balancer/describe/describe_test.go @@ -7,10 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const ( @@ -21,7 +23,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -61,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest)) loadbalancer.ApiGetLoadBalancerRequest { - request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testloadBalancerName) + request := testClient.DefaultAPI.GetLoadBalancer(testCtx, testProjectId, testRegion, testloadBalancerName) for _, mod := range mods { mod(&request) } @@ -129,54 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,7 +157,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -234,11 +189,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.loadBalancer); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.loadBalancer); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/generate-payload/generate_payload.go b/internal/cmd/load-balancer/generate-payload/generate_payload.go index 42cbb4a59..5fe97b3c5 100644 --- a/internal/cmd/load-balancer/generate-payload/generate_payload.go +++ b/internal/cmd/load-balancer/generate-payload/generate_payload.go @@ -5,7 +5,10 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/spf13/cobra" ) @@ -34,9 +36,9 @@ type inputModel struct { var ( defaultPayloadListener = &loadbalancer.Listener{ DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), + Port: utils.Ptr(int32(0)), Protocol: loadbalancer.ListenerProtocol("").Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -57,18 +59,18 @@ var ( defaultPayloadTargetPool = &loadbalancer.TargetPool{ ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(0)), + HealthyThreshold: utils.Ptr(int32(0)), Interval: utils.Ptr(""), IntervalJitter: utils.Ptr(""), Timeout: utils.Ptr(""), - UnhealthyThreshold: utils.Ptr(int64(0)), + UnhealthyThreshold: utils.Ptr(int32(0)), }, Name: utils.Ptr(""), SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(false), }, - TargetPort: utils.Ptr(int64(0)), - Targets: &[]loadbalancer.Target{ + TargetPort: utils.Ptr(int32(0)), + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr(""), Ip: utils.Ptr(""), @@ -78,16 +80,16 @@ var ( DefaultCreateLoadBalancerPayload = loadbalancer.CreateLoadBalancerPayload{ ExternalAddress: utils.Ptr(""), - Listeners: &[]loadbalancer.Listener{ + Listeners: []loadbalancer.Listener{ *defaultPayloadListener, }, Name: utils.Ptr(""), - Networks: &[]loadbalancer.Network{ + Networks: []loadbalancer.Network{ *defaultPayloadNetwork, }, Options: &loadbalancer.LoadBalancerOptions{ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: &[]string{ + AllowedSourceRanges: []string{ "", }, }, @@ -104,19 +106,19 @@ var ( }, PrivateNetworkOnly: utils.Ptr(false), }, - TargetPools: &[]loadbalancer.TargetPool{ + TargetPools: []loadbalancer.TargetPool{ *defaultPayloadTargetPool, }, } ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "generate-payload", Short: "Generates a payload to create/update a Load Balancer", Long: fmt.Sprintf("%s\n%s", "Generates a JSON payload with values to be used as --payload input for load balancer creation or update.", - "See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.", + "See https://docs.api.stackit.cloud/documentation/load-balancer/version/v2#tag/Load-Balancer/operation/APIService_CreateLoadBalancer for information regarding the payload structure.", ), Args: args.NoArgs, Example: examples.Build( @@ -134,9 +136,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Generate a payload with values of an existing load balancer, and preview it in the terminal`, `$ stackit load-balancer generate-payload --lb-name xxx`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -181,7 +183,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(filePathFlag, "f", "", "If set, writes the payload to the given file. If unset, writes the payload to the standard output") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) loadBalancerName := flags.FlagToStringPointer(p, cmd, loadBalancerNameFlag) @@ -196,20 +198,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { FilePath: flags.FlagToStringPointer(p, cmd, filePathFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiGetLoadBalancerRequest { - req := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, *model.LoadBalancerName) + req := apiClient.DefaultAPI.GetLoadBalancer(ctx, model.ProjectId, model.Region, *model.LoadBalancerName) return req } @@ -255,12 +249,12 @@ func outputUpdateResult(p *print.Printer, filePath *string, payload *loadbalance return nil } -func modifyListener(resp *loadbalancer.LoadBalancer) *[]loadbalancer.Listener { - listeners := *resp.Listeners +func modifyListener(resp *loadbalancer.LoadBalancer) []loadbalancer.Listener { + listeners := resp.Listeners for i := range listeners { listeners[i].Name = nil } - return &listeners + return listeners } diff --git a/internal/cmd/load-balancer/generate-payload/generate_payload_test.go b/internal/cmd/load-balancer/generate-payload/generate_payload_test.go index 66a0891ec..8e72d149f 100644 --- a/internal/cmd/load-balancer/generate-payload/generate_payload_test.go +++ b/internal/cmd/load-balancer/generate-payload/generate_payload_test.go @@ -4,14 +4,16 @@ import ( "context" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -21,7 +23,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() const ( @@ -59,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest)) loadbalancer.ApiGetLoadBalancerRequest { - request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) + request := testClient.DefaultAPI.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) for _, mod := range mods { mod(&request) } @@ -69,6 +71,7 @@ func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -132,54 +135,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +160,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -217,18 +173,18 @@ func TestModifyListeners(t *testing.T) { tests := []struct { description string response *loadbalancer.LoadBalancer - expected *[]loadbalancer.Listener + expected []loadbalancer.Listener }{ { description: "base", response: &loadbalancer.LoadBalancer{ - Listeners: &[]loadbalancer.Listener{ + Listeners: []loadbalancer.Listener{ { DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), + Port: utils.Ptr(int32(0)), Protocol: loadbalancer.ListenerProtocol("").Ptr(), Name: utils.Ptr(""), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -243,10 +199,10 @@ func TestModifyListeners(t *testing.T) { }, { DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), + Port: utils.Ptr(int32(0)), Protocol: loadbalancer.ListenerProtocol("").Ptr(), Name: utils.Ptr(""), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -261,13 +217,13 @@ func TestModifyListeners(t *testing.T) { }, }, }, - expected: &[]loadbalancer.Listener{ + expected: []loadbalancer.Listener{ { DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), + Port: utils.Ptr(int32(0)), Protocol: loadbalancer.ListenerProtocol("").Ptr(), Name: nil, - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -282,10 +238,10 @@ func TestModifyListeners(t *testing.T) { }, { DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), + Port: utils.Ptr(int32(0)), Protocol: loadbalancer.ListenerProtocol("").Ptr(), Name: nil, - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -337,11 +293,10 @@ func TestOutputCreateResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputCreateResult(p, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { + if err := outputCreateResult(params.Printer, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { t.Errorf("outputCreateResult() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -371,11 +326,10 @@ func TestOutputUpdateResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputUpdateResult(p, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { + if err := outputUpdateResult(params.Printer, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { t.Errorf("outputUpdateResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/list/list.go b/internal/cmd/load-balancer/list/list.go index 02a1f30ed..4ff11e958 100644 --- a/internal/cmd/load-balancer/list/list.go +++ b/internal/cmd/load-balancer/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Load Balancers", @@ -48,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 load balancers `, "$ stackit load-balancer list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,23 +67,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("get load balancers: %w", err) } - if resp.LoadBalancers == nil || (resp.LoadBalancers != nil && len(*resp.LoadBalancers) == 0) { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No load balancers found for project %q\n", projectLabel) - return nil + loadBalancers := resp.LoadBalancers + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - loadBalancers := *resp.LoadBalancers // Truncate output if model.Limit != nil && len(loadBalancers) > int(*model.Limit) { loadBalancers = loadBalancers[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, loadBalancers) + return outputResult(params.Printer, model.OutputFormat, projectLabel, loadBalancers) }, } @@ -96,7 +92,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -115,52 +111,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiListLoadBalancersRequest { - req := apiClient.ListLoadBalancers(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListLoadBalancers(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, loadBalancers []loadbalancer.LoadBalancer) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(loadBalancers, "", " ") - if err != nil { - return fmt.Errorf("marshal load balancer list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, loadBalancers []loadbalancer.LoadBalancer) error { + return p.OutputResult(outputFormat, loadBalancers, func() error { + if len(loadBalancers) == 0 { + p.Outputf("No load balancers found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(loadBalancers, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal load balancer list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("NAME", "STATE", "IP ADDRESS", "LISTENERS", "TARGET POOLS") for i := range loadBalancers { l := loadBalancers[i] var numListeners, numTargetPools int if l.Listeners != nil { - numListeners = len(*l.Listeners) + numListeners = len(l.Listeners) } if l.TargetPools != nil { - numTargetPools = len(*l.TargetPools) + numTargetPools = len(l.TargetPools) } externalAddress := utils.PtrStringDefault(l.ExternalAddress, "-") @@ -178,5 +153,5 @@ func outputResult(p *print.Printer, outputFormat string, loadBalancers []loadbal } return nil - } + }) } diff --git a/internal/cmd/load-balancer/list/list_test.go b/internal/cmd/load-balancer/list/list_test.go index f43d4ae62..024f6d551 100644 --- a/internal/cmd/load-balancer/list/list_test.go +++ b/internal/cmd/load-balancer/list/list_test.go @@ -7,11 +7,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -21,7 +23,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -52,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiListLoadBalancersRequest)) loadbalancer.ApiListLoadBalancersRequest { - request := testClient.ListLoadBalancers(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListLoadBalancers(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -62,6 +64,7 @@ func fixtureRequest(mods ...func(request *loadbalancer.ApiListLoadBalancersReque func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -116,46 +119,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +143,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +155,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string loadBalancers []loadbalancer.LoadBalancer } tests := []struct { @@ -218,11 +183,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.loadBalancers); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.loadBalancers); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/load_balancer.go b/internal/cmd/load-balancer/load_balancer.go index 4a2876453..25a8f34ae 100644 --- a/internal/cmd/load-balancer/load_balancer.go +++ b/internal/cmd/load-balancer/load_balancer.go @@ -10,7 +10,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/quota" targetpool "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -18,7 +18,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "load-balancer", Aliases: []string{"lb"}, @@ -31,7 +31,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/load-balancer/observability-credentials/add/add.go b/internal/cmd/load-balancer/observability-credentials/add/add.go index 533a222f4..3a02d1d28 100644 --- a/internal/cmd/load-balancer/observability-credentials/add/add.go +++ b/internal/cmd/load-balancer/observability-credentials/add/add.go @@ -2,12 +2,12 @@ package add import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +19,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -35,7 +35,7 @@ type inputModel struct { Password *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "add", Short: "Adds observability credentials to Load Balancer", @@ -49,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Add observability credentials to a load balancer with username "xxx" and display name "yyy", providing the path to a file with the password as flag`, "$ stackit load-balancer observability-credentials add --username xxx --password @./password.txt --display-name yyy"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,21 +68,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - // Prompt for password if not passed in as a flag - if model.Password == nil { - pwd, err := params.Printer.PromptForPassword("Enter user password: ") - if err != nil { - return fmt.Errorf("prompt for password: %w", err) - } - model.Password = utils.Ptr(pwd) - } - - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to add observability credentials for Load Balancer on project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to add observability credentials for Load Balancer on project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -95,20 +84,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) }, } - configureFlags(cmd) + configureFlags(cmd, params) return cmd } -func configureFlags(cmd *cobra.Command) { +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { cmd.Flags().String(displayNameFlag, "", "Credentials display name") cmd.Flags().String(usernameFlag, "", "Username") - cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`) + password := flags.SecretFlag(passwordFlag, params) + cmd.Flags().Var(password, passwordFlag, password.Usage()) err := flags.MarkFlagsRequired(cmd, displayNameFlag, usernameFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,23 +108,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), Username: flags.FlagToStringPointer(p, cmd, usernameFlag), - Password: flags.FlagToStringPointer(p, cmd, passwordFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.Region) req = req.XRequestID(uuid.NewString()) req = req.CreateCredentialsPayload(loadbalancer.CreateCredentialsPayload{ @@ -150,25 +132,8 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *loa return fmt.Errorf("nil observability credentials response") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Load Balancer observability credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Load Balancer observability credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { p.Outputf("Added Load Balancer observability credentials on project %q. Credentials reference: %q\n", projectLabel, utils.PtrString(resp.Credential.CredentialsRef)) return nil - } + }) } diff --git a/internal/cmd/load-balancer/observability-credentials/add/add_test.go b/internal/cmd/load-balancer/observability-credentials/add/add_test.go index bedebaf95..8cd8007c5 100644 --- a/internal/cmd/load-balancer/observability-credentials/add/add_test.go +++ b/internal/cmd/load-balancer/observability-credentials/add/add_test.go @@ -7,11 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -21,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -56,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiCreateCredentialsRequest)) loadbalancer.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testRegion) request = request.CreateCredentialsPayload(loadbalancer.CreateCredentialsPayload{ DisplayName: utils.Ptr("name"), Username: utils.Ptr("username"), @@ -71,6 +72,7 @@ func fixtureRequest(mods ...func(request *loadbalancer.ApiCreateCredentialsReque func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -125,46 +127,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -188,8 +151,8 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), - cmpopts.IgnoreFields(loadbalancer.CreateCredentialsRequest{}, "xRequestID"), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), + cmpopts.IgnoreFields(loadbalancer.ApiCreateCredentialsRequest{}, "xRequestID"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -229,11 +192,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go index 3bcf0190a..756163db6 100644 --- a/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go +++ b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,14 +16,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "cleanup", Short: "Deletes observability credentials unused by any Load Balancer", @@ -33,9 +34,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Delete observability credentials unused by any Load Balancer`, "$ stackit load-balancer observability-credentials cleanup"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -59,8 +60,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } var credentials []loadbalancer.CredentialsResponse - if resp.Credentials != nil && len(*resp.Credentials) > 0 { - credentials, err = utils.FilterCredentials(ctx, apiClient, *resp.Credentials, model.ProjectId, model.Region, utils.OP_FILTER_UNUSED) + if len(resp.Credentials) > 0 { + credentials, err = utils.FilterCredentials(ctx, apiClient.DefaultAPI, resp.Credentials, model.ProjectId, model.Region, utils.OP_FILTER_UNUSED) if err != nil { return fmt.Errorf("filter Load Balancer observability credentials: %w", err) } @@ -71,21 +72,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return nil } - if !model.AssumeYes { - prompt := "Will delete the following unused observability credentials: \n" - for _, credential := range credentials { - if credential.DisplayName == nil || credential.Username == nil { - return fmt.Errorf("list unused Load Balancer observability credentials: credentials %q missing display name or username", *credential.CredentialsRef) - } - name := *credential.DisplayName - username := *credential.Username - prompt += fmt.Sprintf(" - %s (username: %q)\n", name, username) - } - prompt += fmt.Sprintf("Are you sure you want to delete unused observability credentials on project %q? (This cannot be undone)", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err + prompt := "Will delete the following unused observability credentials: \n" + for _, credential := range credentials { + if credential.DisplayName == nil || credential.Username == nil { + return fmt.Errorf("list unused Load Balancer observability credentials: credentials %q missing display name or username", *credential.CredentialsRef) } + name := *credential.DisplayName + username := *credential.Username + prompt += fmt.Sprintf(" - %s (username: %q)\n", name, username) + } + prompt += fmt.Sprintf("Are you sure you want to delete unused observability credentials on project %q? (This cannot be undone)", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } for _, credential := range credentials { @@ -108,7 +107,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,24 +117,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildDeleteCredentialRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient, credentialsRef string) loadbalancer.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.Region, credentialsRef) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.Region, credentialsRef) return req } func buildListCredentialsRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.Region) return req } diff --git a/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go index 7154b55ee..321312e4c 100644 --- a/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go +++ b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -51,7 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureDeleteCredentialRequest(mods ...func(request *loadbalancer.ApiDeleteCredentialsRequest)) loadbalancer.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) for _, mod := range mods { mod(&request) } @@ -59,7 +59,7 @@ func fixtureDeleteCredentialRequest(mods ...func(request *loadbalancer.ApiDelete } func fixtureListCredentialsRequest(mods ...func(request *loadbalancer.ApiListCredentialsRequest)) loadbalancer.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -116,54 +116,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -187,7 +140,7 @@ func TestBuildDeleteCredentialRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -215,7 +168,7 @@ func TestListCredentialsRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/observability-credentials/delete/delete.go b/internal/cmd/load-balancer/observability-credentials/delete/delete.go index bef2362b9..0f50ade43 100644 --- a/internal/cmd/load-balancer/observability-credentials/delete/delete.go +++ b/internal/cmd/load-balancer/observability-credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( loadbalancerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { CredentialsRef string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsRefArg), Short: "Deletes observability credentials for Load Balancer", @@ -57,18 +58,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - credentialsLabel, err := loadbalancerUtils.GetCredentialsDisplayName(ctx, apiClient, model.ProjectId, model.Region, model.CredentialsRef) + credentialsLabel, err := loadbalancerUtils.GetCredentialsDisplayName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.CredentialsRef) if err != nil { params.Printer.Debug(print.ErrorLevel, "get observability credentials display name: %v", err) credentialsLabel = model.CredentialsRef } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete observability credentials %q on project %q?(This cannot be undone)", credentialsLabel, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete observability credentials %q on project %q?(This cannot be undone)", credentialsLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -98,19 +97,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsRef: credentialsRef, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) return req } diff --git a/internal/cmd/load-balancer/observability-credentials/delete/delete_test.go b/internal/cmd/load-balancer/observability-credentials/delete/delete_test.go index 4730f6a31..80e359e2c 100644 --- a/internal/cmd/load-balancer/observability-credentials/delete/delete_test.go +++ b/internal/cmd/load-balancer/observability-credentials/delete/delete_test.go @@ -4,10 +4,10 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -62,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiDeleteCredentialsRequest)) loadbalancer.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) for _, mod := range mods { mod(&request) } @@ -136,54 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -207,7 +160,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/observability-credentials/describe/describe.go b/internal/cmd/load-balancer/observability-credentials/describe/describe.go index 85c32ba7f..244e1e1a0 100644 --- a/internal/cmd/load-balancer/observability-credentials/describe/describe.go +++ b/internal/cmd/load-balancer/observability-credentials/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -29,7 +28,7 @@ type inputModel struct { CredentialsRef string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialsRefArg), Short: "Shows details of observability credentials for Load Balancer", @@ -79,42 +78,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsRef: credentialsRef, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiGetCredentialsRequest { - req := apiClient.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) + req := apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) return req } func outputResult(p *print.Printer, outputFormat string, credentials *loadbalancer.GetCredentialsResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal Load Balancer observability credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Load Balancer observability credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, credentials, func() error { if credentials == nil || credentials.Credential == nil { return fmt.Errorf("credentials response is empty") } @@ -132,5 +106,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials *loadbalanc } return nil - } + }) } diff --git a/internal/cmd/load-balancer/observability-credentials/describe/describe_test.go b/internal/cmd/load-balancer/observability-credentials/describe/describe_test.go index 55335f68b..9c1658c89 100644 --- a/internal/cmd/load-balancer/observability-credentials/describe/describe_test.go +++ b/internal/cmd/load-balancer/observability-credentials/describe/describe_test.go @@ -7,10 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const ( @@ -21,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -61,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiGetCredentialsRequest)) loadbalancer.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) for _, mod := range mods { mod(&request) } @@ -135,54 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -206,7 +160,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -245,11 +199,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/observability-credentials/list/list.go b/internal/cmd/load-balancer/observability-credentials/list/list.go index 714364c47..db3a7394c 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +20,6 @@ import ( lbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -36,7 +36,7 @@ type inputModel struct { Unused bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists observability credentials for Load Balancer", @@ -59,9 +59,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Load Balancer observability credentials`, "$ stackit load-balancer observability-credentials list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -84,37 +84,35 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list Load Balancer observability credentials: %w", err) } - credentialsPtr := resp.Credentials - - var credentials []loadbalancer.CredentialsResponse - if credentialsPtr != nil && len(*credentialsPtr) > 0 { - credentials = *credentialsPtr - filterOp, err := getFilterOp(model.Used, model.Unused) - if err != nil { - return err - } - credentials, err = lbUtils.FilterCredentials(ctx, apiClient, credentials, model.ProjectId, model.Region, filterOp) - if err != nil { - return fmt.Errorf("filter credentials: %w", err) - } + + credentials := resp.Credentials + + filterOp, err := getFilterOp(model.Used, model.Unused) + if err != nil { + return err } - if len(credentials) == 0 { - opLabel := "No " - if model.Used { - opLabel += "used" - } else if model.Unused { - opLabel += "unused" - } - params.Printer.Info("%s observability credentials found for Load Balancer on project %q\n", opLabel, projectLabel) - return nil + filteredCredentials, err := lbUtils.FilterCredentials(ctx, apiClient.DefaultAPI, credentials, model.ProjectId, model.Region, filterOp) + if err != nil { + return fmt.Errorf("filter credentials: %w", err) + } + if filteredCredentials != nil { // lbUtils.FilterCredentials can return nil with no error, if credentials is an empty slice and filterOp is not 0 (if either the --used or the --unused is set) + credentials = filteredCredentials } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + + opLabel := "No" + if model.Used { + opLabel += " used" + } else if model.Unused { + opLabel += " unused" + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, opLabel, credentials) }, } configureFlags(cmd) @@ -129,7 +127,7 @@ func configureFlags(cmd *cobra.Command) { cmd.MarkFlagsMutuallyExclusive(usedFlag, unusedFlag) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -150,42 +148,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Unused: flags.FlagToBoolValue(p, cmd, unusedFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []loadbalancer.CredentialsResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal Load Balancer observability credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel, opLabel string, credentials []loadbalancer.CredentialsResponse) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("%s observability credentials found for Load Balancer on project %q\n", opLabel, projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Load Balancer observability credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("REFERENCE", "DISPLAY NAME", "USERNAME") for i := range credentials { @@ -198,7 +176,7 @@ func outputResult(p *print.Printer, outputFormat string, credentials []loadbalan } return nil - } + }) } func getFilterOp(used, unused bool) (int, error) { diff --git a/internal/cmd/load-balancer/observability-credentials/list/list_test.go b/internal/cmd/load-balancer/observability-credentials/list/list_test.go index 00d107c2e..a7fcd6abf 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list_test.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list_test.go @@ -7,12 +7,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" lbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -22,7 +23,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -53,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiListCredentialsRequest)) loadbalancer.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +64,7 @@ func fixtureRequest(mods ...func(request *loadbalancer.ApiListCredentialsRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -145,54 +147,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -216,7 +171,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -277,6 +232,8 @@ func TestGetFilterOp(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + opLabel string + projectLabel string credentials []loadbalancer.CredentialsResponse } tests := []struct { @@ -297,11 +254,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.opLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/observability-credentials/observability-credentials.go b/internal/cmd/load-balancer/observability-credentials/observability-credentials.go index 03613a8de..7abc80f62 100644 --- a/internal/cmd/load-balancer/observability-credentials/observability-credentials.go +++ b/internal/cmd/load-balancer/observability-credentials/observability-credentials.go @@ -7,14 +7,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/list" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "observability-credentials", Short: "Provides functionality for Load Balancer observability credentials", @@ -27,7 +27,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(add.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/load-balancer/observability-credentials/update/update.go b/internal/cmd/load-balancer/observability-credentials/update/update.go index 29761043e..5dadff828 100644 --- a/internal/cmd/load-balancer/observability-credentials/update/update.go +++ b/internal/cmd/load-balancer/observability-credentials/update/update.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,10 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" loadBalancerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -28,16 +28,6 @@ const ( credentialsRefArg = "CREDENTIALS_REF" //nolint:gosec // linter false positive ) -// enforce implementation of interfaces -var ( - _ loadBalancerClient = &loadbalancer.APIClient{} -) - -type loadBalancerClient interface { - UpdateCredentials(ctx context.Context, projectId, region, credentialsRef string) loadbalancer.ApiUpdateCredentialsRequest - GetCredentialsExecute(ctx context.Context, projectId, region, credentialsRef string) (*loadbalancer.GetCredentialsResponse, error) -} - type inputModel struct { *globalflags.GlobalFlagModel CredentialsRef string @@ -46,7 +36,7 @@ type inputModel struct { Password *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", credentialsRefArg), Short: "Updates observability credentials for Load Balancer", @@ -79,31 +69,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - credentialsLabel, err := loadBalancerUtils.GetCredentialsDisplayName(ctx, apiClient, model.ProjectId, model.Region, model.CredentialsRef) + credentialsLabel, err := loadBalancerUtils.GetCredentialsDisplayName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.CredentialsRef) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials display name: %v", err) credentialsLabel = model.CredentialsRef } - // Prompt for password if not passed in as a flag - if model.Password == nil { - pwd, err := params.Printer.PromptForPassword("Enter new password: ") - if err != nil { - return fmt.Errorf("prompt for password: %w", err) - } - model.Password = utils.Ptr(pwd) - } - - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update observability credentials %q for Load Balancer on project %q?", credentialsLabel, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update observability credentials %q for Load Balancer on project %q?", credentialsLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -117,14 +96,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return nil }, } - configureFlags(cmd) + configureFlags(cmd, params) return cmd } -func configureFlags(cmd *cobra.Command) { +func configureFlags(cmd *cobra.Command, params *types.CmdParams) { cmd.Flags().String(displayNameFlag, "", "Credentials name") cmd.Flags().String(usernameFlag, "", "Username") - cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`) + password := flags.SecretFlag(passwordFlag, params) + cmd.Flags().Var(password, passwordFlag, password.Usage()) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -137,7 +117,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu displayName := flags.FlagToStringPointer(p, cmd, displayNameFlag) username := flags.FlagToStringPointer(p, cmd, usernameFlag) - password := flags.FlagToStringPointer(p, cmd, passwordFlag) + password := flags.SecretFlagToStringPointer(p, cmd, passwordFlag) return &inputModel{ GlobalFlagModel: globalFlags, @@ -148,10 +128,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient loadBalancerClient) (loadbalancer.ApiUpdateCredentialsRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient loadbalancer.DefaultAPI) (loadbalancer.ApiUpdateCredentialsRequest, error) { req := apiClient.UpdateCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef) - currentCredentials, err := apiClient.GetCredentialsExecute(ctx, model.ProjectId, model.Region, model.CredentialsRef) + currentCredentials, err := apiClient.GetCredentials(ctx, model.ProjectId, model.Region, model.CredentialsRef).Execute() if err != nil { return req, fmt.Errorf("get Load Balancer observability credentials: %w", err) } diff --git a/internal/cmd/load-balancer/observability-credentials/update/update_test.go b/internal/cmd/load-balancer/observability-credentials/update/update_test.go index 5aa4faa50..3e7187bcd 100644 --- a/internal/cmd/load-balancer/observability-credentials/update/update_test.go +++ b/internal/cmd/load-balancer/observability-credentials/update/update_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) const ( @@ -25,24 +24,24 @@ type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &loadbalancer.APIClient{} + testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} testProjectId = uuid.NewString() ) -type loadBalancerClientMocked struct { +type mockSettings struct { getCredentialsError bool getCredentialsResponse *loadbalancer.GetCredentialsResponse } -func (c *loadBalancerClientMocked) UpdateCredentials(ctx context.Context, projectId, region, credentialsRef string) loadbalancer.ApiUpdateCredentialsRequest { - return testClient.UpdateCredentials(ctx, projectId, region, credentialsRef) -} - -func (c *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { - if c.getCredentialsError { - return nil, fmt.Errorf("get credentials failed") +func newAPIMock(s mockSettings) loadbalancer.DefaultAPI { + return &loadbalancer.DefaultAPIServiceMock{ + GetCredentialsExecuteMock: utils.Ptr(func(_ loadbalancer.ApiGetCredentialsRequest) (*loadbalancer.GetCredentialsResponse, error) { + if s.getCredentialsError { + return nil, fmt.Errorf("get credentials failed") + } + return s.getCredentialsResponse, nil + }), } - return c.getCredentialsResponse, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -88,7 +87,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateCredentialsRequest)) loadbalancer.ApiUpdateCredentialsRequest { - request := testClient.UpdateCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) + request := testClient.DefaultAPI.UpdateCredentials(testCtx, testProjectId, testRegion, testCredentialsRef) request = request.UpdateCredentialsPayload(loadbalancer.UpdateCredentialsPayload{ DisplayName: utils.Ptr("name"), Username: utils.Ptr("username"), @@ -180,54 +179,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -280,11 +232,11 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ getCredentialsError: tt.getCredentialsFails, getCredentialsResponse: tt.getCredentialsResponse, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(client)) if err != nil { if !tt.isValid { @@ -300,6 +252,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(loadbalancer.ApiUpdateCredentialsRequest{}, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/quota/quota.go b/internal/cmd/load-balancer/quota/quota.go index 2925d7f52..a6220c83a 100644 --- a/internal/cmd/load-balancer/quota/quota.go +++ b/internal/cmd/load-balancer/quota/quota.go @@ -2,12 +2,11 @@ package quota import ( "context" - "encoding/json" "fmt" "strconv" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,14 +15,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "quota", Short: "Shows the configured Load Balancer quota", @@ -34,9 +33,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Get the configured load balancer quota for the project`, "$ stackit load-balancer quota"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -59,7 +58,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -69,20 +68,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiGetQuotaRequest { - req := apiClient.GetQuota(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.GetQuota(ctx, model.ProjectId, model.Region) return req } @@ -90,33 +81,15 @@ func outputResult(p *print.Printer, outputFormat string, quota *loadbalancer.Get if quota == nil { return fmt.Errorf("quota response is empty") } - switch outputFormat { - case print.PrettyOutputFormat: + return p.OutputResult(outputFormat, quota, func() error { maxLoadBalancers := "Unlimited" if quota.MaxLoadBalancers != nil && *quota.MaxLoadBalancers != -1 { - maxLoadBalancers = strconv.FormatInt(*quota.MaxLoadBalancers, 10) + maxLoadBalancers = strconv.FormatInt(int64(*quota.MaxLoadBalancers), 10) } p.Outputf("Maximum number of load balancers allowed: %s\n", maxLoadBalancers) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(quota, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal quota: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - details, err := json.MarshalIndent(quota, "", " ") - if err != nil { - return fmt.Errorf("marshal quota: %w", err) - } - - p.Outputln(string(details)) - - return nil - } + }) } diff --git a/internal/cmd/load-balancer/quota/quota_test.go b/internal/cmd/load-balancer/quota/quota_test.go index 16c14e857..349041153 100644 --- a/internal/cmd/load-balancer/quota/quota_test.go +++ b/internal/cmd/load-balancer/quota/quota_test.go @@ -7,10 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const ( @@ -20,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -49,7 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiGetQuotaRequest)) loadbalancer.ApiGetQuotaRequest { - request := testClient.GetQuota(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.GetQuota(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +61,7 @@ func fixtureRequest(mods ...func(request *loadbalancer.ApiGetQuotaRequest)) load func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -94,46 +97,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -159,7 +123,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -194,11 +158,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.quota); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.quota); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/target-pool/add-target/add_target.go b/internal/cmd/load-balancer/target-pool/add-target/add_target.go index 46faf8bb9..029c12a3e 100644 --- a/internal/cmd/load-balancer/target-pool/add-target/add_target.go +++ b/internal/cmd/load-balancer/target-pool/add-target/add_target.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/spf13/cobra" ) @@ -35,7 +37,7 @@ type inputModel struct { IP string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("add-target %s", ipArg), Short: "Adds a target to a target pool", @@ -61,16 +63,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.IP, model.TargetPoolName, model.LBName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to add a target with IP %q to target pool %q of load balancer %q?", model.IP, model.TargetPoolName, model.LBName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("build request: %w", err) } @@ -112,19 +112,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu IP: ip, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient utils.LoadBalancerClient) (loadbalancer.ApiUpdateTargetPoolRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient loadbalancer.DefaultAPI) (loadbalancer.ApiUpdateTargetPoolRequest, error) { req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.Region, model.LBName, model.TargetPoolName) targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.Region, model.LBName, model.TargetPoolName) diff --git a/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go b/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go index 7d7b1799f..12d7c9b4e 100644 --- a/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go +++ b/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go @@ -5,11 +5,12 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -20,7 +21,7 @@ type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &loadbalancer.APIClient{} + testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} testProjectId = uuid.NewString() ) @@ -32,33 +33,29 @@ const ( testIP = "1.1.1.1" ) -type loadBalancerClientMocked struct { +type mockSettings struct { getCredentialsFails bool getCredentialsResp *loadbalancer.GetCredentialsResponse getLoadBalancerFails bool getLoadBalancerResp *loadbalancer.LoadBalancer } -func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get credentials") - } - return m.getCredentialsResp, nil -} - -func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _, _ string) (*loadbalancer.LoadBalancer, error) { - if m.getLoadBalancerFails { - return nil, fmt.Errorf("could not get load balancer") +func newAPIMock(settings mockSettings) loadbalancer.DefaultAPI { + return &loadbalancer.DefaultAPIServiceMock{ + GetCredentialsExecuteMock: utils.Ptr( + func(_ loadbalancer.ApiGetCredentialsRequest) (*loadbalancer.GetCredentialsResponse, error) { + if settings.getCredentialsFails { + return nil, fmt.Errorf("could not get credentials") + } + return settings.getCredentialsResp, nil + }), + GetLoadBalancerExecuteMock: utils.Ptr(func(_ loadbalancer.ApiGetLoadBalancerRequest) (*loadbalancer.LoadBalancer, error) { + if settings.getLoadBalancerFails { + return nil, fmt.Errorf("could not get load balancer") + } + return settings.getLoadBalancerResp, nil + }), } - return m.getLoadBalancerResp, nil -} - -func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, projectId, region, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest { - return testClient.UpdateTargetPool(ctx, projectId, region, loadBalancerName, targetPoolName) -} - -func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { - return nil, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -103,8 +100,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureTargets() *[]loadbalancer.Target { - return &[]loadbalancer.Target{ +func fixtureTargets() []loadbalancer.Target { + return []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -119,21 +116,21 @@ func fixtureTargets() *[]loadbalancer.Target { func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer { lb := loadbalancer.LoadBalancer{ Name: utils.Ptr(testLBName), - TargetPools: &[]loadbalancer.TargetPool{ + TargetPools: []loadbalancer.TargetPool{ { Name: utils.Ptr(testTargetPoolName), Targets: fixtureTargets(), ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - UnhealthyThreshold: utils.Ptr(int64(3)), + UnhealthyThreshold: utils.Ptr(int32(3)), }, SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(true), }, - TargetPort: utils.Ptr(int64(80)), + TargetPort: utils.Ptr(int32(80)), }, { Name: utils.Ptr("target-pool-2"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("6.7.8.9"), @@ -157,12 +154,12 @@ func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) payload := &loadbalancer.UpdateTargetPoolPayload{ Name: utils.Ptr("target-pool-1"), ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - UnhealthyThreshold: utils.Ptr(int64(3)), + UnhealthyThreshold: utils.Ptr(int32(3)), }, SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(true), }, - TargetPort: utils.Ptr(int64(80)), + TargetPort: utils.Ptr(int32(80)), Targets: fixtureTargets(), } @@ -173,7 +170,7 @@ func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) } func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateTargetPoolRequest)) loadbalancer.ApiUpdateTargetPoolRequest { - request := testClient.UpdateTargetPool(testCtx, testProjectId, testRegion, testLBName, testTargetPoolName) + request := testClient.DefaultAPI.UpdateTargetPool(testCtx, testProjectId, testRegion, testLBName, testTargetPoolName) request = request.UpdateTargetPoolPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -260,8 +257,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { @@ -294,7 +291,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -329,54 +326,54 @@ func TestBuildRequest(t *testing.T) { isValid: true, expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) { payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) { - payload.Targets = &[]loadbalancer.Target{ - (*fixtureTargets())[0], - (*fixtureTargets())[1], + payload.Targets = []loadbalancer.Target{ + (fixtureTargets())[0], + (fixtureTargets())[1], { DisplayName: utils.Ptr(testTargetName), Ip: utils.Ptr(testIP), }, } }) - *request = (*request).UpdateTargetPoolPayload(*payload) + *request = request.UpdateTargetPoolPayload(*payload) }), }, { description: "empty targets", model: fixtureInputModel(), getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - (*lb.TargetPools)[0].Targets = &[]loadbalancer.Target{} + (lb.TargetPools)[0].Targets = []loadbalancer.Target{} }), isValid: true, expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) { payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) { - payload.Targets = &[]loadbalancer.Target{ + payload.Targets = []loadbalancer.Target{ { DisplayName: utils.Ptr(testTargetName), Ip: utils.Ptr(testIP), }, } }) - *request = (*request).UpdateTargetPoolPayload(*payload) + *request = request.UpdateTargetPoolPayload(*payload) }), }, { description: "nil targets", model: fixtureInputModel(), getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - (*lb.TargetPools)[0].Targets = nil + (lb.TargetPools)[0].Targets = nil }), isValid: true, expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) { payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) { - payload.Targets = &[]loadbalancer.Target{ + payload.Targets = []loadbalancer.Target{ { DisplayName: utils.Ptr(testTargetName), Ip: utils.Ptr(testIP), }, } }) - *request = (*request).UpdateTargetPoolPayload(*payload) + *request = request.UpdateTargetPoolPayload(*payload) }), }, { @@ -398,7 +395,7 @@ func TestBuildRequest(t *testing.T) { description: "nil target pool", model: fixtureInputModel(), getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - *lb.TargetPools = nil + lb.TargetPools = nil }), isValid: false, }, @@ -406,11 +403,11 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ getLoadBalancerFails: tt.getLoadBalancerFails, getLoadBalancerResp: tt.getLoadBalancerResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(client)) if err != nil { if !tt.isValid { return @@ -421,6 +418,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(loadbalancer.ApiUpdateTargetPoolRequest{}, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/target-pool/describe/describe.go b/internal/cmd/load-balancer/target-pool/describe/describe.go index ea0f6dace..2e4dd4018 100644 --- a/internal/cmd/load-balancer/target-pool/describe/describe.go +++ b/internal/cmd/load-balancer/target-pool/describe/describe.go @@ -2,14 +2,15 @@ package describe import ( "context" - "encoding/json" "fmt" "strconv" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,7 +21,6 @@ import ( lbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) const ( @@ -35,7 +35,7 @@ type inputModel struct { LBName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", targetPoolNameArg), Short: "Shows details of a target pool in a Load Balancer", @@ -68,12 +68,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read load balancer: %w", err) } - targetPool := lbUtils.FindLoadBalancerTargetPoolByName(*resp.TargetPools, model.TargetPoolName) + targetPool := lbUtils.FindLoadBalancerTargetPoolByName(resp.TargetPools, model.TargetPoolName) if targetPool == nil { return fmt.Errorf("target pool not found") } - listener := lbUtils.FindLoadBalancerListenerByTargetPool(*resp.Listeners, *targetPool.Name) + listener := lbUtils.FindLoadBalancerListenerByTargetPool(resp.Listeners, *targetPool.Name) return outputResult(params.Printer, model.OutputFormat, *targetPool, listener) }, @@ -103,20 +103,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu LBName: cmd.Flag(lbNameFlag).Value.String(), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiGetLoadBalancerRequest { - req := apiClient.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.LBName) + req := apiClient.DefaultAPI.GetLoadBalancer(ctx, model.ProjectId, model.Region, model.LBName) return req } @@ -132,90 +124,69 @@ func outputResult(p *print.Printer, outputFormat string, targetPool loadbalancer listener, } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(output, "", " ") - if err != nil { - return fmt.Errorf("marshal load balancer: %w", err) + return p.OutputResult(outputFormat, output, func() error { + sessionPersistence := "None" + if targetPool.SessionPersistence != nil && targetPool.SessionPersistence.UseSourceIpAddress != nil && *targetPool.SessionPersistence.UseSourceIpAddress { + sessionPersistence = "Use Source IP" } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal load balancer: %w", err) + healthCheckInterval := "-" + healthCheckUnhealthyThreshold := "-" + healthCheckHealthyThreshold := "-" + if targetPool.ActiveHealthCheck != nil { + if targetPool.ActiveHealthCheck.Interval != nil { + healthCheckInterval = *targetPool.ActiveHealthCheck.Interval + } + if targetPool.ActiveHealthCheck.UnhealthyThreshold != nil { + healthCheckUnhealthyThreshold = strconv.FormatInt(int64(*targetPool.ActiveHealthCheck.UnhealthyThreshold), 10) + } + if targetPool.ActiveHealthCheck.HealthyThreshold != nil { + healthCheckHealthyThreshold = strconv.FormatInt(int64(*targetPool.ActiveHealthCheck.HealthyThreshold), 10) + } } - p.Outputln(string(details)) - return nil - default: - return outputResultAsTable(p, targetPool, listener) - } -} - -func outputResultAsTable(p *print.Printer, targetPool loadbalancer.TargetPool, listener *loadbalancer.Listener) error { - sessionPersistence := "None" - if targetPool.SessionPersistence != nil && targetPool.SessionPersistence.UseSourceIpAddress != nil && *targetPool.SessionPersistence.UseSourceIpAddress { - sessionPersistence = "Use Source IP" - } - - healthCheckInterval := "-" - healthCheckUnhealthyThreshold := "-" - healthCheckHealthyThreshold := "-" - if targetPool.ActiveHealthCheck != nil { - if targetPool.ActiveHealthCheck.Interval != nil { - healthCheckInterval = *targetPool.ActiveHealthCheck.Interval - } - if targetPool.ActiveHealthCheck.UnhealthyThreshold != nil { - healthCheckUnhealthyThreshold = strconv.FormatInt(*targetPool.ActiveHealthCheck.UnhealthyThreshold, 10) - } - if targetPool.ActiveHealthCheck.HealthyThreshold != nil { - healthCheckHealthyThreshold = strconv.FormatInt(*targetPool.ActiveHealthCheck.HealthyThreshold, 10) + targets := "-" + if targetPool.Targets != nil { + var targetsSlice []string + for _, target := range targetPool.Targets { + targetStr := fmt.Sprintf("%s (%s)", *target.DisplayName, *target.Ip) + targetsSlice = append(targetsSlice, targetStr) + } + targets = strings.Join(targetsSlice, "\n") } - } - targets := "-" - if targetPool.Targets != nil { - var targetsSlice []string - for _, target := range *targetPool.Targets { - targetStr := fmt.Sprintf("%s (%s)", *target.DisplayName, *target.Ip) - targetsSlice = append(targetsSlice, targetStr) + listenerStr := "-" + if listener != nil { + listenerStr = fmt.Sprintf("%s (Port:%s, Protocol: %s)", + utils.PtrString(listener.Name), + utils.PtrString(listener.Port), + utils.PtrString(listener.Protocol), + ) } - targets = strings.Join(targetsSlice, "\n") - } - - listenerStr := "-" - if listener != nil { - listenerStr = fmt.Sprintf("%s (Port:%s, Protocol: %s)", - utils.PtrString(listener.Name), - utils.PtrString(listener.Port), - utils.PtrString(listener.Protocol), - ) - } - table := tables.NewTable() - table.AddRow("NAME", utils.PtrString(targetPool.Name)) - table.AddSeparator() - table.AddRow("TARGET PORT", utils.PtrString(targetPool.TargetPort)) - table.AddSeparator() - table.AddRow("ATTACHED LISTENER", listenerStr) - table.AddSeparator() - table.AddRow("TARGETS", targets) - table.AddSeparator() - table.AddRow("SESSION PERSISTENCE", sessionPersistence) - table.AddSeparator() - table.AddRow("HEALTH CHECK INTERVAL", healthCheckInterval) - table.AddSeparator() - table.AddRow("HEALTH CHECK DOWN AFTER", healthCheckUnhealthyThreshold) - table.AddSeparator() - table.AddRow("HEALTH CHECK UP AFTER", healthCheckHealthyThreshold) - table.AddSeparator() - - err := p.PagerDisplay(table.Render()) - if err != nil { - return fmt.Errorf("display output: %w", err) - } + table := tables.NewTable() + table.AddRow("NAME", utils.PtrString(targetPool.Name)) + table.AddSeparator() + table.AddRow("TARGET PORT", utils.PtrString(targetPool.TargetPort)) + table.AddSeparator() + table.AddRow("ATTACHED LISTENER", listenerStr) + table.AddSeparator() + table.AddRow("TARGETS", targets) + table.AddSeparator() + table.AddRow("SESSION PERSISTENCE", sessionPersistence) + table.AddSeparator() + table.AddRow("HEALTH CHECK INTERVAL", healthCheckInterval) + table.AddSeparator() + table.AddRow("HEALTH CHECK DOWN AFTER", healthCheckUnhealthyThreshold) + table.AddSeparator() + table.AddRow("HEALTH CHECK UP AFTER", healthCheckHealthyThreshold) + table.AddSeparator() + + err := p.PagerDisplay(table.Render()) + if err != nil { + return fmt.Errorf("display output: %w", err) + } - return nil + return nil + }) } diff --git a/internal/cmd/load-balancer/target-pool/describe/describe_test.go b/internal/cmd/load-balancer/target-pool/describe/describe_test.go index d59a0fe6b..8e70da65d 100644 --- a/internal/cmd/load-balancer/target-pool/describe/describe_test.go +++ b/internal/cmd/load-balancer/target-pool/describe/describe_test.go @@ -7,17 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &loadbalancer.APIClient{} + testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} testProjectId = uuid.NewString() ) @@ -66,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiGetLoadBalancerRequest)) loadbalancer.ApiGetLoadBalancerRequest { - request := testClient.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) + request := testClient.DefaultAPI.GetLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) for _, mod := range mods { mod(&request) } @@ -136,8 +137,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { @@ -170,7 +171,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -210,7 +211,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -244,11 +245,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.targetPool, tt.args.listener); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.targetPool, tt.args.listener); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go b/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go index 3ca65c79b..320eacffa 100644 --- a/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go +++ b/internal/cmd/load-balancer/target-pool/remove-target/remove_target.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/spf13/cobra" ) @@ -33,7 +35,7 @@ type inputModel struct { IP string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("remove-target %s", ipArg), Short: "Removes a target from a target pool", @@ -57,22 +59,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - targetLabel, err := utils.GetTargetName(ctx, apiClient, model.ProjectId, model.Region, model.LBName, model.TargetPoolName, model.IP) + targetLabel, err := utils.GetTargetName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.LBName, model.TargetPoolName, model.IP) if err != nil { params.Printer.Debug(print.ErrorLevel, "get target name: %v", err) targetLabel = model.IP } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to remove target %q from target pool %q of load balancer %q?", targetLabel, model.TargetPoolName, model.LBName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to remove target %q from target pool %q of load balancer %q?", targetLabel, model.TargetPoolName, model.LBName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("build request: %w", err) } @@ -112,19 +112,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu IP: ip, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient utils.LoadBalancerClient) (loadbalancer.ApiUpdateTargetPoolRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient loadbalancer.DefaultAPI) (loadbalancer.ApiUpdateTargetPoolRequest, error) { req := apiClient.UpdateTargetPool(ctx, model.ProjectId, model.Region, model.LBName, model.TargetPoolName) targetPool, err := utils.GetLoadBalancerTargetPool(ctx, apiClient, model.ProjectId, model.Region, model.LBName, model.TargetPoolName) diff --git a/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go index 14bb4fadc..6559c3848 100644 --- a/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go +++ b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go @@ -5,11 +5,12 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -20,7 +21,7 @@ type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &loadbalancer.APIClient{} + testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} testProjectId = uuid.NewString() ) @@ -31,33 +32,28 @@ const ( testIP = "1.2.3.4" ) -type loadBalancerClientMocked struct { +type mockSettings struct { getCredentialsFails bool getCredentialsResp *loadbalancer.GetCredentialsResponse getLoadBalancerFails bool getLoadBalancerResp *loadbalancer.LoadBalancer } -func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get credentials") - } - return m.getCredentialsResp, nil -} - -func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _, _ string) (*loadbalancer.LoadBalancer, error) { - if m.getLoadBalancerFails { - return nil, fmt.Errorf("could not get load balancer") +func newAPIMock(s mockSettings) loadbalancer.DefaultAPI { + return loadbalancer.DefaultAPIServiceMock{ + GetCredentialsExecuteMock: utils.Ptr(func(_ loadbalancer.ApiGetCredentialsRequest) (*loadbalancer.GetCredentialsResponse, error) { + if s.getCredentialsFails { + return nil, fmt.Errorf("could not get credentials") + } + return s.getCredentialsResp, nil + }), + GetLoadBalancerExecuteMock: utils.Ptr(func(_ loadbalancer.ApiGetLoadBalancerRequest) (*loadbalancer.LoadBalancer, error) { + if s.getLoadBalancerFails { + return nil, fmt.Errorf("could not get load balancer") + } + return s.getLoadBalancerResp, nil + }), } - return m.getLoadBalancerResp, nil -} - -func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, projectId, region, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest { - return testClient.UpdateTargetPool(ctx, projectId, region, loadBalancerName, targetPoolName) -} - -func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { - return nil, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -100,8 +96,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureTargets() *[]loadbalancer.Target { - return &[]loadbalancer.Target{ +func fixtureTargets() []loadbalancer.Target { + return []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -116,21 +112,21 @@ func fixtureTargets() *[]loadbalancer.Target { func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer { lb := loadbalancer.LoadBalancer{ Name: utils.Ptr(testLBName), - TargetPools: &[]loadbalancer.TargetPool{ + TargetPools: []loadbalancer.TargetPool{ { Name: utils.Ptr(testTargetPoolName), Targets: fixtureTargets(), ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - UnhealthyThreshold: utils.Ptr(int64(3)), + UnhealthyThreshold: utils.Ptr(int32(3)), }, SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(true), }, - TargetPort: utils.Ptr(int64(80)), + TargetPort: utils.Ptr(int32(80)), }, { Name: utils.Ptr("target-pool-2"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("6.7.8.9"), @@ -154,12 +150,12 @@ func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) payload := &loadbalancer.UpdateTargetPoolPayload{ Name: utils.Ptr("target-pool-1"), ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - UnhealthyThreshold: utils.Ptr(int64(3)), + UnhealthyThreshold: utils.Ptr(int32(3)), }, SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(true), }, - TargetPort: utils.Ptr(int64(80)), + TargetPort: utils.Ptr(int32(80)), Targets: fixtureTargets(), } @@ -170,7 +166,7 @@ func fixturePayload(mods ...func(payload *loadbalancer.UpdateTargetPoolPayload)) } func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateTargetPoolRequest)) loadbalancer.ApiUpdateTargetPoolRequest { - request := testClient.UpdateTargetPool(testCtx, testProjectId, testRegion, testLBName, testTargetPoolName) + request := testClient.DefaultAPI.UpdateTargetPool(testCtx, testProjectId, testRegion, testLBName, testTargetPoolName) request = request.UpdateTargetPoolPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -249,8 +245,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { @@ -283,7 +279,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -318,16 +314,16 @@ func TestBuildRequest(t *testing.T) { isValid: true, expectedRequest: fixtureRequest(func(request *loadbalancer.ApiUpdateTargetPoolRequest) { payload := fixturePayload(func(payload *loadbalancer.UpdateTargetPoolPayload) { - payload.Targets = utils.Ptr((*payload.Targets)[1:]) + payload.Targets = (payload.Targets)[1:] }) - *request = (*request).UpdateTargetPoolPayload(*payload) + *request = request.UpdateTargetPoolPayload(*payload) }), }, { description: "empty targets", model: fixtureInputModel(), getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - (*lb.TargetPools)[0].Targets = &[]loadbalancer.Target{} + (lb.TargetPools)[0].Targets = []loadbalancer.Target{} }), isValid: false, }, @@ -344,7 +340,7 @@ func TestBuildRequest(t *testing.T) { description: "nil targets", model: fixtureInputModel(), getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - (*lb.TargetPools)[0].Targets = nil + (lb.TargetPools)[0].Targets = nil }), isValid: false, }, @@ -367,7 +363,7 @@ func TestBuildRequest(t *testing.T) { description: "nil target pool", model: fixtureInputModel(), getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - *lb.TargetPools = nil + lb.TargetPools = nil }), isValid: false, }, @@ -375,11 +371,11 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ getLoadBalancerFails: tt.getLoadBalancerFails, getLoadBalancerResp: tt.getLoadBalancerResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(client)) if err != nil { if !tt.isValid { return @@ -390,6 +386,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(loadbalancer.ApiUpdateTargetPoolRequest{}, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/load-balancer/target-pool/target_pool.go b/internal/cmd/load-balancer/target-pool/target_pool.go index da2e0f7fa..7e40f76e7 100644 --- a/internal/cmd/load-balancer/target-pool/target_pool.go +++ b/internal/cmd/load-balancer/target-pool/target_pool.go @@ -4,14 +4,14 @@ import ( addtarget "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/add-target" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/describe" removetarget "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/target-pool/remove-target" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "target-pool", Short: "Provides functionality for target pools", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(addtarget.NewCmd(params)) cmd.AddCommand(removetarget.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/load-balancer/update/update.go b/internal/cmd/load-balancer/update/update.go index db183702d..ed625c41f 100644 --- a/internal/cmd/load-balancer/update/update.go +++ b/internal/cmd/load-balancer/update/update.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -29,14 +30,14 @@ type inputModel struct { Payload loadbalancer.UpdateLoadBalancerPayload } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", loadBalancerNameArg), Short: "Updates a Load Balancer", Long: fmt.Sprintf("%s\n%s\n%s", "Updates a load balancer.", "The payload can be provided as a JSON string or a file path prefixed with \"@\".", - "See https://docs.api.stackit.cloud/documentation/load-balancer/version/v1#tag/Load-Balancer/operation/APIService_UpdateLoadBalancer for information regarding the payload structure.", + "See https://docs.api.stackit.cloud/documentation/load-balancer/version/v2#tag/Load-Balancer/operation/APIService_UpdateLoadBalancer for information regarding the payload structure.", ), Args: args.SingleArg(loadBalancerNameArg, nil), Example: examples.Build( @@ -65,12 +66,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update load balancer %q?", model.LoadBalancerName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update load balancer %q?", model.LoadBalancerName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -117,20 +116,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Payload: payload, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiUpdateLoadBalancerRequest { - req := apiClient.UpdateLoadBalancer(ctx, model.ProjectId, model.Region, model.LoadBalancerName) + req := apiClient.DefaultAPI.UpdateLoadBalancer(ctx, model.ProjectId, model.Region, model.LoadBalancerName) req = req.UpdateLoadBalancerPayload(model.Payload) return req diff --git a/internal/cmd/load-balancer/update/update_test.go b/internal/cmd/load-balancer/update/update_test.go index b57d57336..d2828ffd5 100644 --- a/internal/cmd/load-balancer/update/update_test.go +++ b/internal/cmd/load-balancer/update/update_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -23,18 +22,18 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &loadbalancer.APIClient{} +var testClient = &loadbalancer.APIClient{DefaultAPI: &loadbalancer.DefaultAPIService{}} var testProjectId = uuid.NewString() var testPayload = loadbalancer.UpdateLoadBalancerPayload{ ExternalAddress: utils.Ptr(""), - Listeners: &[]loadbalancer.Listener{ + Listeners: []loadbalancer.Listener{ { DisplayName: utils.Ptr(""), - Port: utils.Ptr(int64(0)), - Protocol: loadbalancer.ListenerProtocol("").Ptr(), - ServerNameIndicators: &[]loadbalancer.ServerNameIndicator{ + Port: utils.Ptr(int32(0)), + Protocol: loadbalancer.ListenerProtocol("unknown_default_open_api").Ptr(), + ServerNameIndicators: []loadbalancer.ServerNameIndicator{ { Name: utils.Ptr(""), }, @@ -49,15 +48,15 @@ var testPayload = loadbalancer.UpdateLoadBalancerPayload{ }, }, Name: utils.Ptr(""), - Networks: &[]loadbalancer.Network{ + Networks: []loadbalancer.Network{ { NetworkId: utils.Ptr(""), - Role: loadbalancer.NetworkRole("").Ptr(), + Role: loadbalancer.NetworkRole("unknown_default_open_api").Ptr(), }, }, Options: &loadbalancer.LoadBalancerOptions{ AccessControl: &loadbalancer.LoadbalancerOptionAccessControl{ - AllowedSourceRanges: &[]string{ + AllowedSourceRanges: []string{ "", }, }, @@ -74,21 +73,21 @@ var testPayload = loadbalancer.UpdateLoadBalancerPayload{ }, PrivateNetworkOnly: utils.Ptr(false), }, - TargetPools: &[]loadbalancer.TargetPool{ + TargetPools: []loadbalancer.TargetPool{ { ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - HealthyThreshold: utils.Ptr(int64(0)), + HealthyThreshold: utils.Ptr(int32(0)), Interval: utils.Ptr(""), IntervalJitter: utils.Ptr(""), Timeout: utils.Ptr(""), - UnhealthyThreshold: utils.Ptr(int64(0)), + UnhealthyThreshold: utils.Ptr(int32(0)), }, Name: utils.Ptr(""), SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(false), }, - TargetPort: utils.Ptr(int64(0)), - Targets: &[]loadbalancer.Target{ + TargetPort: utils.Ptr(int32(0)), + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr(""), Ip: utils.Ptr(""), @@ -210,7 +209,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *loadbalancer.ApiUpdateLoadBalancerRequest)) loadbalancer.ApiUpdateLoadBalancerRequest { - request := testClient.UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) + request := testClient.DefaultAPI.UpdateLoadBalancer(testCtx, testProjectId, testRegion, testLoadBalancerName) request = request.UpdateLoadBalancerPayload(testPayload) for _, mod := range mods { mod(&request) @@ -300,54 +299,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithOptions(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, nil, tt.isValid, []testutils.TestingOption{ + testutils.WithCmpOptions(cmpopts.EquateEmpty()), + }) }) } } @@ -369,10 +323,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) - diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, loadbalancer.DefaultAPIService{}), + cmpopts.EquateEmpty(), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/logme/credentials/create/create.go b/internal/cmd/logme/credentials/create/create.go index c1e44ccff..0c5fdd1e3 100644 --- a/internal/cmd/logme/credentials/create/create.go +++ b/internal/cmd/logme/credentials/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/client" logmeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/logme" ) const ( @@ -31,7 +31,7 @@ type inputModel struct { ShowPassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for a LogMe instance", @@ -45,31 +45,28 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for a LogMe instance and show the password in the output`, "$ stackit logme credentials create --instance-id xxx --show-password"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } - // Configure API client apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } - instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -106,20 +103,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ShowPassword: flags.FlagToBoolValue(p, cmd, showPasswordFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.InstanceId) return req } @@ -128,42 +117,26 @@ func outputResult(p *print.Printer, outputFormat string, showPassword bool, inst return fmt.Errorf("credentials response is empty") } - if !showPassword && resp.HasRaw() && resp.Raw.Credentials != nil { - resp.Raw.Credentials.Password = utils.Ptr("hidden") + if !showPassword && resp.HasRaw() { + resp.Raw.Credentials.Password = "hidden" } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe credentials: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, utils.PtrString(resp.Id)) + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, resp.Id) // The username field cannot be set by the user so we only display it if it's not returned empty - if resp.HasRaw() && resp.Raw.Credentials != nil { - if username := resp.Raw.Credentials.Username; username != nil && *username != "" { - p.Outputf("Username: %s\n", utils.PtrString(username)) + if resp.HasRaw() { + if username := resp.Raw.Credentials.Username; username != "" { + p.Outputf("Username: %s\n", username) } if !showPassword { p.Outputf("Password: \n") } else { - p.Outputf("Password: %s\n", utils.PtrString(resp.Raw.Credentials.Password)) + p.Outputf("Password: %s\n", resp.Raw.Credentials.Password) } - p.Outputf("Host: %s\n", utils.PtrString(resp.Raw.Credentials.Host)) + p.Outputf("Host: %s\n", resp.Raw.Credentials.Host) p.Outputf("Port: %s\n", utils.PtrString(resp.Raw.Credentials.Port)) } - p.Outputf("URI: %s\n", utils.PtrString(resp.Uri)) + p.Outputf("URI: %s\n", resp.Uri) return nil - } + }) } diff --git a/internal/cmd/logme/credentials/create/create_test.go b/internal/cmd/logme/credentials/create/create_test.go index d577d1422..87dc6fd13 100644 --- a/internal/cmd/logme/credentials/create/create_test.go +++ b/internal/cmd/logme/credentials/create/create_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -49,7 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiCreateCredentialsRequest)) logme.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -59,6 +59,7 @@ func fixtureRequest(mods ...func(request *logme.ApiCreateCredentialsRequest)) lo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -130,46 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -193,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -227,11 +189,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.showPassword, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.showPassword, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logme/credentials/credentials.go b/internal/cmd/logme/credentials/credentials.go index 9f7cd2d7e..51f821a68 100644 --- a/internal/cmd/logme/credentials/credentials.go +++ b/internal/cmd/logme/credentials/credentials.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/logme/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/logme/credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/logme/credentials/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for LogMe credentials", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/logme/credentials/delete/delete.go b/internal/cmd/logme/credentials/delete/delete.go index fabd2b838..95247df08 100644 --- a/internal/cmd/logme/credentials/delete/delete.go +++ b/internal/cmd/logme/credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsIdArg), Short: "Deletes credentials of a LogMe instance", @@ -55,24 +56,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - credentialsLabel, err := logmeUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialsLabel, err := logmeUtils.GetCredentialsUsername(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.CredentialsId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials username: %v", err) credentialsLabel = model.CredentialsId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,19 +110,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) return req } diff --git a/internal/cmd/logme/credentials/delete/delete_test.go b/internal/cmd/logme/credentials/delete/delete_test.go index 96aff20b2..8842fd6a9 100644 --- a/internal/cmd/logme/credentials/delete/delete_test.go +++ b/internal/cmd/logme/credentials/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,10 +18,11 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() +var testRegion = "region" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -36,8 +36,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -49,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -61,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiDeleteCredentialsRequest)) logme.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -165,54 +167,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +191,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/logme/credentials/describe/describe.go b/internal/cmd/logme/credentials/describe/describe.go index 240df0f81..ea65abdc2 100644 --- a/internal/cmd/logme/credentials/describe/describe.go +++ b/internal/cmd/logme/credentials/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialsIdArg), Short: "Shows details of credentials of a LogMe instance", @@ -95,20 +94,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiGetCredentialsRequest { - req := apiClient.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) return req } @@ -117,38 +108,21 @@ func outputResult(p *print.Printer, outputFormat string, credentials *logme.Cred return fmt.Errorf("credentials is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, credentials, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(credentials.Id)) + table.AddRow("ID", credentials.Id) table.AddSeparator() // The username field cannot be set by the user so we only display it if it's not returned empty if raw := credentials.Raw; raw != nil { - if cred := raw.Credentials; cred != nil { - if username := cred.Username; username != nil && *username != "" { - table.AddRow("USERNAME", *username) - table.AddSeparator() - } - table.AddRow("PASSWORD", utils.PtrString(cred.Password)) + cred := raw.Credentials + + if username := cred.Username; username != "" { + table.AddRow("USERNAME", username) table.AddSeparator() - table.AddRow("URI", utils.PtrString(cred.Uri)) } + table.AddRow("PASSWORD", cred.Password) + table.AddSeparator() + table.AddRow("URI", utils.PtrString(cred.Uri)) } err := table.Display(p) if err != nil { @@ -156,5 +130,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials *logme.Cred } return nil - } + }) } diff --git a/internal/cmd/logme/credentials/describe/describe_test.go b/internal/cmd/logme/credentials/describe/describe_test.go index ae7f38696..20e706f8d 100644 --- a/internal/cmd/logme/credentials/describe/describe_test.go +++ b/internal/cmd/logme/credentials/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() @@ -61,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiGetCredentialsRequest)) logme.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -165,54 +165,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +189,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -268,11 +221,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logme/credentials/list/list.go b/internal/cmd/logme/credentials/list/list.go index 218dff2d1..355b10a29 100644 --- a/internal/cmd/logme/credentials/list/list.go +++ b/internal/cmd/logme/credentials/list/list.go @@ -2,11 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,10 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/client" logmeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" ) const ( @@ -33,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials' IDs for a LogMe instance", @@ -50,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials' IDs for a LogMe instance`, "$ stackit logme credentials list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,22 +67,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list LogMe credentials: %w", err) } - credentials := *resp.CredentialsList - if len(credentials) == 0 { - instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId - } - params.Printer.Info("No credentials found for instance %q\n", instanceLabel) - return nil + credentials := resp.GetCredentialsList() + + instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, credentials) }, } configureFlags(cmd) @@ -99,7 +94,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -119,47 +114,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []logme.CredentialsListItem) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, credentials []logme.CredentialsListItem) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID") for i := range credentials { c := credentials[i] - table.AddRow(utils.PtrString(c.Id)) + table.AddRow(c.Id) } err := table.Display(p) if err != nil { @@ -167,5 +142,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials []logme.Cre } return nil - } + }) } diff --git a/internal/cmd/logme/credentials/list/list_test.go b/internal/cmd/logme/credentials/list/list_test.go index 55c522bbf..1f2de370e 100644 --- a/internal/cmd/logme/credentials/list/list_test.go +++ b/internal/cmd/logme/credentials/list/list_test.go @@ -4,31 +4,31 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +var testRegion = "region" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -40,6 +40,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -52,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiListCredentialsRequest)) logme.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -62,6 +63,7 @@ func fixtureRequest(mods ...func(request *logme.ApiListCredentialsRequest)) logm func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -80,21 +82,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,46 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -211,8 +174,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - credentials []logme.CredentialsListItem + outputFormat string + instanceLabel string + credentials []logme.CredentialsListItem } tests := []struct { name string @@ -239,11 +203,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logme/instance/create/create.go b/internal/cmd/logme/instance/create/create.go index ca00f4cdc..ba0d1aa0a 100644 --- a/internal/cmd/logme/instance/create/create.go +++ b/internal/cmd/logme/instance/create/create.go @@ -2,13 +2,12 @@ package create import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +21,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api/wait" ) const ( @@ -48,15 +47,15 @@ type inputModel struct { InstanceName *string EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a LogMe instance", @@ -73,9 +72,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a LogMe instance with name "my-instance" and specify IP range which is allowed to access it`, "$ stackit logme instance create --name my-instance --plan-id xxx --acl 1.2.3.0/24"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -92,16 +91,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a LogMe instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a LogMe instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -113,17 +110,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create LogMe instance: %w", err) } - instanceId := *resp.InstanceId + instanceId := resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for LogMe instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) @@ -137,7 +134,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") @@ -150,7 +147,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -177,39 +174,26 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { EnableMonitoring: flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag), MonitoringInstanceId: flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag), Graphite: flags.FlagToStringPointer(p, cmd, graphiteFlag), - MetricsFrequency: flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag), + MetricsFrequency: flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag), MetricsPrefix: flags.FlagToStringPointer(p, cmd, metricsPrefixFlag), SgwAcl: flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag), - Syslog: flags.FlagToStringSlicePointer(p, cmd, syslogFlag), + Syslog: flags.FlagToStringSliceValue(p, cmd, syslogFlag), PlanId: planId, PlanName: planName, Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type logMeClient interface { - CreateInstance(ctx context.Context, projectId string) logme.ApiCreateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*logme.ListOfferingsResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient logMeClient) (logme.ApiCreateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient logme.DefaultAPI) (logme.ApiCreateInstanceRequest, error) { req := apiClient.CreateInstance(ctx, model.ProjectId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get LogMe offerings: %w", err) } @@ -237,7 +221,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient logMeClient) } req = req.CreateInstancePayload(logme.CreateInstancePayload{ - InstanceName: model.InstanceName, + InstanceName: utils.PtrValue(model.InstanceName), Parameters: &logme.InstanceParameters{ EnableMonitoring: model.EnableMonitoring, Graphite: model.Graphite, @@ -247,7 +231,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient logMeClient) SgwAcl: sgwAcl, Syslog: model.Syslog, }, - PlanId: planId, + PlanId: utils.PtrValue(planId), }) return req, nil } @@ -257,29 +241,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe return fmt.Errorf("response is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } - p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, utils.PtrString(resp.InstanceId)) + p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, resp.InstanceId) return nil - } + }) } diff --git a/internal/cmd/logme/instance/create/create_test.go b/internal/cmd/logme/instance/create/create_test.go index 9f452d086..7180961db 100644 --- a/internal/cmd/logme/instance/create/create_test.go +++ b/internal/cmd/logme/instance/create/create_test.go @@ -5,15 +5,15 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,22 +21,22 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} -type logMeClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *logme.ListOfferingsResponse } -func (c *logMeClientMocked) CreateInstance(ctx context.Context, projectId string) logme.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId) -} - -func (c *logMeClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*logme.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) logme.DefaultAPI { + return &logme.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ logme.ApiListOfferingsRequest) (*logme.ListOfferingsResponse, error) { + if s.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return s.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var testProjectId = uuid.NewString() @@ -71,12 +71,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { InstanceName: utils.Ptr("example-name"), EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), - MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + MonitoringInstanceId: &testMonitoringInstanceId, SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), - PlanId: utils.Ptr(testPlanId), + Syslog: []string{"example-syslog"}, + PlanId: &testPlanId, } for _, mod := range mods { mod(model) @@ -85,19 +85,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiCreateInstanceRequest)) logme.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId) request = request.CreateInstancePayload(logme.CreateInstancePayload{ - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", Parameters: &logme.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), - MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + MonitoringInstanceId: &testMonitoringInstanceId, SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }) for _, mod := range mods { mod(&request) @@ -108,6 +108,7 @@ func fixtureRequest(mods ...func(request *logme.ApiCreateInstanceRequest)) logme func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string sgwAclValues []string syslogValues []string @@ -153,7 +154,7 @@ func TestParseInput(t *testing.T) { Verbosity: globalflags.VerbosityDefault, }, InstanceName: utils.Ptr("example-name"), - PlanId: utils.Ptr(testPlanId), + PlanId: &testPlanId, }, }, { @@ -173,11 +174,11 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - PlanId: utils.Ptr(testPlanId), + PlanId: &testPlanId, InstanceName: utils.Ptr(""), EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -253,75 +254,17 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + syslogFlag: tt.syslogValues, + }, tt.isValid) }) } } @@ -340,13 +283,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), getOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -364,13 +307,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), getOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -399,13 +342,13 @@ func TestBuildRequest(t *testing.T) { }, ), getOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -420,33 +363,33 @@ func TestBuildRequest(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - PlanId: utils.Ptr(testPlanId), + PlanId: &testPlanId, }, getOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, }, }, - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(logme.CreateInstancePayload{PlanId: utils.Ptr(testPlanId), Parameters: &logme.InstanceParameters{}}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId). + CreateInstancePayload(logme.CreateInstancePayload{PlanId: testPlanId, Parameters: &logme.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &logMeClientMocked{ + settings := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.getOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(settings)) if err != nil { if !tt.isValid { return @@ -457,6 +400,8 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateEmpty(), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -490,11 +435,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logme/instance/delete/delete.go b/internal/cmd/logme/instance/delete/delete.go index 905c51bed..60c23993b 100644 --- a/internal/cmd/logme/instance/delete/delete.go +++ b/internal/cmd/logme/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a LogMe instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for LogMe instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.InstanceId) return req } diff --git a/internal/cmd/logme/instance/delete/delete_test.go b/internal/cmd/logme/instance/delete/delete_test.go index dde9637bb..f3376c4c0 100644 --- a/internal/cmd/logme/instance/delete/delete_test.go +++ b/internal/cmd/logme/instance/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiDeleteInstanceRequest)) logme.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +161,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/logme/instance/describe/describe.go b/internal/cmd/logme/instance/describe/describe.go index bf7892373..6803c7e50 100644 --- a/internal/cmd/logme/instance/describe/describe.go +++ b/internal/cmd/logme/instance/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a LogMe instance", @@ -83,20 +82,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId) return req } @@ -105,39 +96,22 @@ func outputResult(p *print.Printer, outputFormat string, instance *logme.Instanc return fmt.Errorf("instance is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(instance.InstanceId)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - if instance.LastOperation != nil { - table.AddRow("LAST OPERATION TYPE", utils.PtrString(instance.LastOperation.Type)) + if _, ok := instance.LastOperation.GetStateOk(); ok { + table.AddRow("LAST OPERATION TYPE", instance.LastOperation.Type) table.AddSeparator() - table.AddRow("LAST OPERATION STATE", utils.PtrString(instance.LastOperation.State)) + table.AddRow("LAST OPERATION STATE", instance.LastOperation.State) table.AddSeparator() } - table.AddRow("PLAN ID", utils.PtrString(instance.PlanId)) + table.AddRow("PLAN ID", instance.PlanId) // Only show ACL if it's present and not empty if instance.Parameters != nil { - acl := (*instance.Parameters)[aclParameterKey] + acl := (instance.Parameters)[aclParameterKey] aclStr, ok := acl.(string) if ok && aclStr != "" { table.AddSeparator() @@ -150,5 +124,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *logme.Instanc } return nil - } + }) } diff --git a/internal/cmd/logme/instance/describe/describe_test.go b/internal/cmd/logme/instance/describe/describe_test.go index 00041b4a9..7d7c08456 100644 --- a/internal/cmd/logme/instance/describe/describe_test.go +++ b/internal/cmd/logme/instance/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiGetInstanceRequest)) logme.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -241,11 +194,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logme/instance/instance.go b/internal/cmd/logme/instance/instance.go index aa86b4f08..184c1b27b 100644 --- a/internal/cmd/logme/instance/instance.go +++ b/internal/cmd/logme/instance/instance.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/logme/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/logme/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/logme/instance/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for LogMe instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/logme/instance/list/list.go b/internal/cmd/logme/instance/list/list.go index e325aee95..f1f89b472 100644 --- a/internal/cmd/logme/instance/list/list.go +++ b/internal/cmd/logme/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/logme" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all LogMe instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 LogMe instances`, "$ stackit logme instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +66,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get LogMe instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + instances := resp.GetInstances() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,56 +110,36 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, instances []logme.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []logme.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "LAST OPERATION TYPE", "LAST OPERATION STATE") for i := range instances { instance := instances[i] - lastOperationType, lastOperationState := "", "" - if instance.LastOperation != nil { - lastOperationType = utils.PtrString(instance.LastOperation.Type) - lastOperationState = utils.PtrString(instance.LastOperation.State) + lastOperationType, lastOperationState := logme.INSTANCELASTOPERATIONTYPE_UNKNOWN_DEFAULT_OPEN_API, logme.INSTANCELASTOPERATIONSTATE_UNKNOWN_DEFAULT_OPEN_API + if _, ok := instance.LastOperation.GetStateOk(); ok { + lastOperationType = instance.LastOperation.Type + lastOperationState = instance.LastOperation.State } table.AddRow( utils.PtrString(instance.InstanceId), - utils.PtrString(instance.Name), + instance.Name, lastOperationType, lastOperationState, ) @@ -173,5 +150,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []logme.Insta } return nil - } + }) } diff --git a/internal/cmd/logme/instance/list/list_test.go b/internal/cmd/logme/instance/list/list_test.go index 0168ee409..ca24cc560 100644 --- a/internal/cmd/logme/instance/list/list_test.go +++ b/internal/cmd/logme/instance/list/list_test.go @@ -4,30 +4,27 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -50,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiListInstancesRequest)) logme.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +57,7 @@ func fixtureRequest(mods ...func(request *logme.ApiListInstancesRequest)) logme. func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +76,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -114,48 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +136,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +148,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []logme.Instance } tests := []struct { @@ -218,11 +176,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logme/instance/update/update.go b/internal/cmd/logme/instance/update/update.go index a7e3ec443..681d27d86 100644 --- a/internal/cmd/logme/instance/update/update.go +++ b/internal/cmd/logme/instance/update/update.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" - "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api/wait" ) const ( @@ -32,7 +33,6 @@ const ( metricsFrequencyFlag = "metrics-frequency" metricsPrefixFlag = "metrics-prefix" monitoringInstanceIdFlag = "monitoring-instance-id" - pluginFlag = "plugin" sgwAclFlag = "acl" syslogFlag = "syslog" planIdFlag = "plan-id" @@ -48,15 +48,16 @@ type inputModel struct { EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string + InstanceName *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a LogMe instance", @@ -83,22 +84,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := logmeUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -114,13 +113,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for LogMe instance update: %w", err) } - s.Stop() } operationState := "Updated" @@ -138,7 +137,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") @@ -146,6 +145,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") cmd.Flags().String(planNameFlag, "", "Plan name") cmd.Flags().String(versionFlag, "", "Instance LogMe version") + cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -159,13 +159,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu enableMonitoring := flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag) monitoringInstanceId := flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag) graphite := flags.FlagToStringPointer(p, cmd, graphiteFlag) - metricsFrequency := flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag) + metricsFrequency := flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag) metricsPrefix := flags.FlagToStringPointer(p, cmd, metricsPrefixFlag) sgwAcl := flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag) - syslog := flags.FlagToStringSlicePointer(p, cmd, syslogFlag) + syslog := flags.FlagToStringSliceValue(p, cmd, syslogFlag) planId := flags.FlagToStringPointer(p, cmd, planIdFlag) planName := flags.FlagToStringValue(p, cmd, planNameFlag) version := flags.FlagToStringValue(p, cmd, versionFlag) + instanceName := flags.FlagToStringPointer(p, cmd, instanceNameFlag) if planId != nil && (planName != "" || version != "") { return nil, &cliErr.DSAInputPlanError{ @@ -175,8 +176,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } if enableMonitoring == nil && monitoringInstanceId == nil && graphite == nil && - metricsFrequency == nil && metricsPrefix == nil && - sgwAcl == nil && syslog == nil && planId == nil && + metricsFrequency == nil && metricsPrefix == nil && sgwAcl == nil && + syslog == nil && planId == nil && instanceName == nil && planName == "" && version == "" { return nil, &cliErr.EmptyUpdateError{} } @@ -194,32 +195,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu PlanId: planId, PlanName: planName, Version: version, + InstanceName: instanceName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type logMeClient interface { - PartialUpdateInstance(ctx context.Context, projectId, instanceId string) logme.ApiPartialUpdateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*logme.ListOfferingsResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient logMeClient) (logme.ApiPartialUpdateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient logme.DefaultAPI) (logme.ApiPartialUpdateInstanceRequest, error) { req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get LogMe offerings: %w", err) } diff --git a/internal/cmd/logme/instance/update/update_test.go b/internal/cmd/logme/instance/update/update_test.go index dc3518985..985e06669 100644 --- a/internal/cmd/logme/instance/update/update_test.go +++ b/internal/cmd/logme/instance/update/update_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,22 +20,22 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} -type logMeClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *logme.ListOfferingsResponse } -func (c *logMeClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId string) logme.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId) -} - -func (c *logMeClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*logme.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) logme.DefaultAPI { + return &logme.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ logme.ApiListOfferingsRequest) (*logme.ListOfferingsResponse, error) { + if s.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return s.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var ( @@ -83,12 +82,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { InstanceId: testInstanceId, EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), - MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + MonitoringInstanceId: &testMonitoringInstanceId, SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), - PlanId: utils.Ptr(testPlanId), + Syslog: []string{"example-syslog"}, + PlanId: &testPlanId, } for _, mod := range mods { mod(model) @@ -97,18 +96,18 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiPartialUpdateInstanceRequest)) logme.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) request = request.PartialUpdateInstancePayload(logme.PartialUpdateInstancePayload{ Parameters: &logme.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), - MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), + MonitoringInstanceId: &testMonitoringInstanceId, SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, - PlanId: utils.Ptr(testPlanId), + PlanId: &testPlanId, }) for _, mod := range mods { mod(&request) @@ -184,10 +183,10 @@ func TestParseInput(t *testing.T) { Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - PlanId: utils.Ptr(testPlanId), + PlanId: &testPlanId, EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -258,17 +257,15 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -320,7 +317,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -353,13 +350,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), listOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -377,13 +374,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), listOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -412,13 +409,13 @@ func TestBuildRequest(t *testing.T) { }, ), listOfferingsResp: &logme.ListOfferingsResponse{ - Offerings: &[]logme.Offering{ + Offerings: []logme.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]logme.Plan{ + Version: "example-version", + Plans: []logme.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -435,18 +432,18 @@ func TestBuildRequest(t *testing.T) { }, InstanceId: testInstanceId, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). PartialUpdateInstancePayload(logme.PartialUpdateInstancePayload{Parameters: &logme.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &logMeClientMocked{ + settings := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.listOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(settings)) if err != nil { if !tt.isValid { return @@ -457,6 +454,8 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateEmpty(), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/logme/logme.go b/internal/cmd/logme/logme.go index a4e3f4715..a1371d7c1 100644 --- a/internal/cmd/logme/logme.go +++ b/internal/cmd/logme/logme.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/logme/credentials" "github.com/stackitcloud/stackit-cli/internal/cmd/logme/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/logme/plans" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "logme", Short: "Provides functionality for LogMe", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(plans.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) diff --git a/internal/cmd/logme/plans/plans.go b/internal/cmd/logme/plans/plans.go index 8c339a1ee..2b8d58e9a 100644 --- a/internal/cmd/logme/plans/plans.go +++ b/internal/cmd/logme/plans/plans.go @@ -2,12 +2,13 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/logme" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists all LogMe service plans", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 LogMe service plans`, "$ stackit logme plans --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +65,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get LogMe service plans: %w", err) } - plans := *resp.Offerings - if len(plans) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No plans found for project %q\n", projectLabel) - return nil + plans := resp.GetOfferings() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +78,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { plans = plans[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, plans) + return outputResult(params.Printer, model.OutputFormat, projectLabel, plans) }, } @@ -94,7 +90,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,55 +109,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APIClient) logme.ApiListOfferingsRequest { - req := apiClient.ListOfferings(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListOfferings(ctx, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, plans []logme.Offering) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("marshal LogMe plans: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal LogMe plans: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []logme.Offering) error { + return p.OutputResult(outputFormat, plans, func() error { + if len(plans) == 0 { + p.Outputf("No plans found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] if o.Plans != nil { - for j := range *o.Plans { - p := (*o.Plans)[j] + for j := range o.Plans { + p := (o.Plans)[j] table.AddRow( - utils.PtrString(o.Name), - utils.PtrString(o.Version), - utils.PtrString(p.Id), - utils.PtrString(p.Name), - utils.PtrString(p.Description), + o.Name, + o.Version, + p.Id, + p.Name, + p.Description, ) } table.AddSeparator() @@ -174,5 +150,5 @@ func outputResult(p *print.Printer, outputFormat string, plans []logme.Offering) } return nil - } + }) } diff --git a/internal/cmd/logme/plans/plans_test.go b/internal/cmd/logme/plans/plans_test.go index 6feb2a2a5..9555d8840 100644 --- a/internal/cmd/logme/plans/plans_test.go +++ b/internal/cmd/logme/plans/plans_test.go @@ -4,30 +4,27 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &logme.APIClient{} +var testClient = &logme.APIClient{DefaultAPI: &logme.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -50,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *logme.ApiListOfferingsRequest)) logme.ApiListOfferingsRequest { - request := testClient.ListOfferings(testCtx, testProjectId) + request := testClient.DefaultAPI.ListOfferings(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +57,7 @@ func fixtureRequest(mods ...func(request *logme.ApiListOfferingsRequest)) logme. func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +76,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -114,48 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +136,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, logme.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +148,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string plans []logme.Offering } tests := []struct { @@ -218,11 +176,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.plans); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/logs/access_token/access_token.go b/internal/cmd/logs/access_token/access_token.go new file mode 100644 index 000000000..1013dfe77 --- /dev/null +++ b/internal/cmd/logs/access_token/access_token.go @@ -0,0 +1,38 @@ +package access_token + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/delete_all" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/delete_all_expired" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "access-token", + Short: "Provides functionality for Logs access-tokens", + Long: "Provides functionality for Logs access-tokens.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(delete_all.NewCmd(params)) + cmd.AddCommand(delete_all_expired.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) +} diff --git a/internal/cmd/logs/access_token/create/create.go b/internal/cmd/logs/access_token/create/create.go new file mode 100644 index 000000000..0cec80e40 --- /dev/null +++ b/internal/cmd/logs/access_token/create/create.go @@ -0,0 +1,159 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" + + "github.com/spf13/cobra" +) + +const ( + displayNameFlag = "display-name" + instanceIdFlag = "instance-id" + lifetimeFlag = "lifetime" + descriptionFlag = "description" + permissionsFlag = "permissions" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + Description *string + DisplayName string + Lifetime *int32 + Permissions []string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a Logs access token", + Long: "Creates a Logs access token.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a access token with the display name "access-token-1" for the instance "xxx" with read and write permissions`, + `$ stackit logs access-token create --display-name access-token-1 --instance-id xxx --permissions read,write`, + ), + examples.NewExample( + `Create a write only access token with a description`, + `$ stackit logs access-token create --display-name access-token-2 --instance-id xxx --permissions write --description "Access token for service"`, + ), + examples.NewExample( + `Create a read only access token which expires in 30 days`, + `$ stackit logs access-token create --display-name access-token-3 --instance-id xxx --permissions read --lifetime 30`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + instanceLabel, err := logsUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId + } + + prompt := fmt.Sprintf("Are you sure you want to create a access token for the Logs instance %q in the project %q?", instanceLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Logs access-token : %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, instanceLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + cmd.Flags().String(displayNameFlag, "", "Display name for the access token") + cmd.Flags().String(descriptionFlag, "", "Description of the access token") + cmd.Flags().Int32(lifetimeFlag, 0, "Lifetime of the access token in days [1 - 180]") + cmd.Flags().StringSlice(permissionsFlag, []string{}, `Permissions of the access token ["read" "write"]`) + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag, displayNameFlag, permissionsFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DisplayName: flags.FlagToStringValue(p, cmd, displayNameFlag), + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Lifetime: flags.FlagToInt32Pointer(p, cmd, lifetimeFlag), + Permissions: flags.FlagToStringSliceValue(p, cmd, permissionsFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiCreateAccessTokenRequest { + req := apiClient.DefaultAPI.CreateAccessToken(ctx, model.ProjectId, model.Region, model.InstanceId) + return req.CreateAccessTokenPayload(logs.CreateAccessTokenPayload{ + Description: model.Description, + DisplayName: model.DisplayName, + Lifetime: model.Lifetime, + Permissions: utils.Map(model.Permissions, func(t string) logs.PermissionsInner { + return logs.PermissionsInner(t) + }), + }) +} + +func outputResult(p *print.Printer, outputFormat, instanceLabel string, accessToken *logs.AccessToken) error { + if accessToken == nil { + return fmt.Errorf("access token cannot be nil") + } + return p.OutputResult(outputFormat, accessToken, func() error { + p.Outputf("Created access token for Logs instance %q.\n\nID: %s\nToken: %s\n", instanceLabel, accessToken.Id, utils.PtrValue(accessToken.AccessToken)) + return nil + }) +} diff --git a/internal/cmd/logs/access_token/create/create_test.go b/internal/cmd/logs/access_token/create/create_test.go new file mode 100644 index 000000000..64053f791 --- /dev/null +++ b/internal/cmd/logs/access_token/create/create_test.go @@ -0,0 +1,280 @@ +package create + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" + + testDisplayName = "display-name" + testDescription = "description" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + displayNameFlag: testDisplayName, + descriptionFlag: testDescription, + permissionsFlag: "read,write", + lifetimeFlag: "0", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + Description: utils.Ptr(testDescription), + DisplayName: testDisplayName, + Lifetime: utils.Ptr(int32(0)), + Permissions: []string{ + "read", + "write", + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiCreateAccessTokenRequest)) logs.ApiCreateAccessTokenRequest { + request := testClient.DefaultAPI.CreateAccessToken(testCtx, testProjectId, testRegion, testInstanceId) + request = request.CreateAccessTokenPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *logs.CreateAccessTokenPayload)) logs.CreateAccessTokenPayload { + payload := logs.CreateAccessTokenPayload{ + DisplayName: testDisplayName, + Description: utils.Ptr(testDescription), + Lifetime: utils.Ptr(int32(0)), + Permissions: []logs.PermissionsInner{ + logs.PERMISSIONSINNER_READ, + logs.PERMISSIONSINNER_WRITE, + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "only required flags", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, lifetimeFlag) + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Lifetime = nil + model.Description = nil + }), + }, + { + description: "one permission", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[permissionsFlag] = "read" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Permissions = []string{ + "read", + } + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "lifetime invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[lifetimeFlag] = "invalid-integer" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + var tests = []struct { + description string + model *inputModel + expectedRequest logs.ApiCreateAccessTokenRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + instanceLabel string + accessToken *logs.AccessToken + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "base", + args: args{ + instanceLabel: "", + accessToken: utils.Ptr(logs.AccessToken{ + Id: uuid.NewString(), + Permissions: []logs.PermissionsInner{ + logs.PERMISSIONSINNER_READ, + logs.PERMISSIONSINNER_WRITE, + }, + DisplayName: "Token", + AccessToken: utils.Ptr("Secret access token"), + Creator: uuid.NewString(), + Expires: false, + Status: logs.ACCESSTOKENSTATUS_ACTIVE, + }), + }, + wantErr: false, + }, + { + name: "empty access token", + args: args{ + instanceLabel: "", + accessToken: utils.Ptr(logs.AccessToken{}), + }, + wantErr: false, + }, + { + name: "empty", + args: args{}, + wantErr: true, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.accessToken); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/access_token/delete/delete.go b/internal/cmd/logs/access_token/delete/delete.go new file mode 100644 index 000000000..62761727e --- /dev/null +++ b/internal/cmd/logs/access_token/delete/delete.go @@ -0,0 +1,117 @@ +package delete + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +const ( + instanceIdFlag = "instance-id" + accessTokenIdArg = "ACCESS_TOKEN_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + AccessTokenId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", accessTokenIdArg), + Short: "Deletes a Logs access token", + Long: "Deletes a Logs access token.", + Args: args.SingleArg(accessTokenIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete access token with ID "xxx" in instance "yyy"`, + "$ stackit logs access-token delete xxx --instance-id yyy", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get the display name for confirmation + accessTokenLabel, err := logUtils.GetAccessTokenName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.AccessTokenId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get access token: %v", err) + } + if accessTokenLabel == "" { + accessTokenLabel = model.AccessTokenId + } + + prompt := fmt.Sprintf("Are you sure you want to delete access token %q?", accessTokenLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete access token: %w", err) + } + + params.Printer.Outputf("Deleted access token %q\n", accessTokenLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + accessTokenId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + AccessTokenId: accessTokenId, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiDeleteAccessTokenRequest { + return apiClient.DefaultAPI.DeleteAccessToken(ctx, model.ProjectId, model.Region, model.InstanceId, model.AccessTokenId) +} diff --git a/internal/cmd/logs/access_token/delete/delete_test.go b/internal/cmd/logs/access_token/delete/delete_test.go new file mode 100644 index 000000000..a81be1bd1 --- /dev/null +++ b/internal/cmd/logs/access_token/delete/delete_test.go @@ -0,0 +1,207 @@ +package delete + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testAccessTokenId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testAccessTokenId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + AccessTokenId: testAccessTokenId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiDeleteAccessTokenRequest)) logs.ApiDeleteAccessTokenRequest { + request := testClient.DefaultAPI.DeleteAccessToken(testCtx, testProjectId, testRegion, testInstanceId, testAccessTokenId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "access token id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "access token id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiDeleteAccessTokenRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/logs/access_token/delete_all/delete_all.go b/internal/cmd/logs/access_token/delete_all/delete_all.go new file mode 100644 index 000000000..8492a2f9b --- /dev/null +++ b/internal/cmd/logs/access_token/delete_all/delete_all.go @@ -0,0 +1,110 @@ +package delete_all + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" +) + +const ( + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete-all", + Short: "Deletes all Logs access token", + Long: "Deletes all Logs access token.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete all access tokens in instance "xxx"`, + "$ stackit logs access-token delete-all --instance-id xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + instanceLabel, err := logUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId + } + + prompt := fmt.Sprintf("Are you sure you want to delete all access tokens for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + items, err := req.Execute() + if err != nil { + return fmt.Errorf("delete all access token: %w", err) + } + if items == nil { + return fmt.Errorf("delete all access token: nil result") + } + + params.Printer.Outputf("Deleted %d access token(s)\n", len(items.Tokens)) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiDeleteAllAccessTokensRequest { + return apiClient.DefaultAPI.DeleteAllAccessTokens(ctx, model.ProjectId, model.Region, model.InstanceId) +} diff --git a/internal/cmd/logs/access_token/delete_all/delete_all_test.go b/internal/cmd/logs/access_token/delete_all/delete_all_test.go new file mode 100644 index 000000000..57551c1f2 --- /dev/null +++ b/internal/cmd/logs/access_token/delete_all/delete_all_test.go @@ -0,0 +1,163 @@ +package delete_all + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiDeleteAllAccessTokensRequest)) logs.ApiDeleteAllAccessTokensRequest { + request := testClient.DefaultAPI.DeleteAllAccessTokens(testCtx, testProjectId, testRegion, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiDeleteAllAccessTokensRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/logs/access_token/delete_all_expired/delete_all_expired.go b/internal/cmd/logs/access_token/delete_all_expired/delete_all_expired.go new file mode 100644 index 000000000..1c4b540d0 --- /dev/null +++ b/internal/cmd/logs/access_token/delete_all_expired/delete_all_expired.go @@ -0,0 +1,110 @@ +package delete_all_expired + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" +) + +const ( + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete-all-expired", + Short: "Deletes all expired Logs access token", + Long: "Deletes all expired Logs access token.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete all expired access tokens in instance "xxx"`, + "$ stackit logs access-token delete-all-expired --instance-id xxx", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + instanceLabel, err := logUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId + } + + prompt := fmt.Sprintf("Are you sure you want to delete all expired access tokens in instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + items, err := req.Execute() + if err != nil { + return fmt.Errorf("delete all expired access token: %w", err) + } + if items == nil { + return fmt.Errorf("delete all expired access token: nil result") + } + + params.Printer.Outputf("Deleted %d expired access token(s)\n", len(items.Tokens)) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiDeleteAllExpiredAccessTokensRequest { + return apiClient.DefaultAPI.DeleteAllExpiredAccessTokens(ctx, model.ProjectId, model.Region, model.InstanceId) +} diff --git a/internal/cmd/logs/access_token/delete_all_expired/delete_all_expired_test.go b/internal/cmd/logs/access_token/delete_all_expired/delete_all_expired_test.go new file mode 100644 index 000000000..20eda0fcf --- /dev/null +++ b/internal/cmd/logs/access_token/delete_all_expired/delete_all_expired_test.go @@ -0,0 +1,163 @@ +package delete_all_expired + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiDeleteAllExpiredAccessTokensRequest)) logs.ApiDeleteAllExpiredAccessTokensRequest { + request := testClient.DefaultAPI.DeleteAllExpiredAccessTokens(testCtx, testProjectId, testRegion, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiDeleteAllExpiredAccessTokensRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/logs/access_token/describe/describe.go b/internal/cmd/logs/access_token/describe/describe.go new file mode 100644 index 000000000..841ed8c27 --- /dev/null +++ b/internal/cmd/logs/access_token/describe/describe.go @@ -0,0 +1,136 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +const ( + instanceIdFlag = "instance-id" + accessTokenIdArg = "ACCESS_TOKEN_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + AccessTokenId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", accessTokenIdArg), + Short: "Shows details of a Logs access token", + Long: "Shows details of a Logs access token.", + Args: args.SingleArg(accessTokenIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Show details of a Logs access token with ID "xxx"`, + "$ stackit logs access-token describe xxx", + ), + examples.NewExample( + `Show details of a Logs access token with ID "xxx" in JSON format`, + "$ stackit logs access-token describe xxx --output-format json", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read access token: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + accessTokenId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + AccessTokenId: accessTokenId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiGetAccessTokenRequest { + return apiClient.DefaultAPI.GetAccessToken(ctx, model.ProjectId, model.Region, model.InstanceId, model.AccessTokenId) +} + +func outputResult(p *print.Printer, outputFormat string, token *logs.AccessToken) error { + if token == nil { + return fmt.Errorf("access token cannot be nil") + } + return p.OutputResult(outputFormat, token, func() error { + table := tables.NewTable() + table.AddRow("ID", token.Id) + table.AddSeparator() + table.AddRow("DISPLAY NAME", token.DisplayName) + table.AddSeparator() + table.AddRow("DESCRIPTION", utils.PtrString(token.Description)) + table.AddSeparator() + table.AddRow("PERMISSIONS", token.Permissions) + table.AddSeparator() + table.AddRow("CREATOR", token.Creator) + table.AddSeparator() + table.AddRow("STATE", token.Status) + table.AddSeparator() + table.AddRow("EXPIRES", token.Expires) + table.AddSeparator() + table.AddRow("VALID UNTIL", token.ValidUntil) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/logs/access_token/describe/describe_test.go b/internal/cmd/logs/access_token/describe/describe_test.go new file mode 100644 index 000000000..e9d4222cc --- /dev/null +++ b/internal/cmd/logs/access_token/describe/describe_test.go @@ -0,0 +1,260 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testAccessTokenId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testAccessTokenId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + AccessTokenId: testAccessTokenId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiGetAccessTokenRequest)) logs.ApiGetAccessTokenRequest { + request := testClient.DefaultAPI.GetAccessToken(testCtx, testProjectId, testRegion, testInstanceId, testAccessTokenId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "access token id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "access token id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiGetAccessTokenRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + accessToken *logs.AccessToken + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "base", + args: args{ + accessToken: utils.Ptr(logs.AccessToken{ + Id: uuid.NewString(), + Permissions: []logs.PermissionsInner{ + logs.PERMISSIONSINNER_READ, + logs.PERMISSIONSINNER_WRITE, + }, + DisplayName: "Token", + AccessToken: utils.Ptr("Secret access token"), + Creator: uuid.NewString(), + Expires: false, + Status: logs.ACCESSTOKENSTATUS_ACTIVE, + }), + }, + wantErr: false, + }, + { + name: "set empty access token", + args: args{ + accessToken: utils.Ptr(logs.AccessToken{}), + }, + wantErr: false, + }, + { + name: "empty", + args: args{}, + wantErr: true, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.accessToken); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/access_token/list/list.go b/internal/cmd/logs/access_token/list/list.go new file mode 100644 index 000000000..0843bc6c2 --- /dev/null +++ b/internal/cmd/logs/access_token/list/list.go @@ -0,0 +1,161 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +const ( + limitFlag = "limit" + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all Logs access tokens of a project", + Long: "Lists all access tokens of a project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists all access tokens of the instance "xxx"`, + "$ stackit logs access-token list --instance-id xxx", + ), + examples.NewExample( + `Lists all access tokens in JSON format`, + "$ stackit logs access-token list --instance-id xxx --output-format json", + ), + examples.NewExample( + `Lists up to 10 access-token`, + "$ stackit logs access-token list --instance-id xxx --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list access tokens: %w", err) + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + // Truncate output + items := resp.Tokens + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, items, projectLabel) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiListAccessTokensRequest { + return apiClient.DefaultAPI.ListAccessTokens(ctx, model.ProjectId, model.Region, model.InstanceId) +} + +func outputResult(p *print.Printer, outputFormat string, tokens []logs.AccessToken, projectLabel string) error { + return p.OutputResult(outputFormat, tokens, func() error { + if len(tokens) == 0 { + p.Outputf("No access token found for project %q\n", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "DESCRIPTION", "PERMISSIONS", "VALID UNTIL", "STATUS") + + for _, token := range tokens { + table.AddRow( + token.Id, + token.DisplayName, + utils.PtrString(token.Description), + token.Permissions, + utils.PtrString(token.ValidUntil), + token.Status, + ) + table.AddSeparator() + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/logs/access_token/list/list_test.go b/internal/cmd/logs/access_token/list/list_test.go new file mode 100644 index 000000000..761b2db9c --- /dev/null +++ b/internal/cmd/logs/access_token/list/list_test.go @@ -0,0 +1,238 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiListAccessTokensRequest)) logs.ApiListAccessTokensRequest { + request := testClient.DefaultAPI.ListAccessTokens(testCtx, testProjectId, testRegion, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiListAccessTokensRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + accessTokens []logs.AccessToken + projectLabel string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "base", + args: args{ + accessTokens: []logs.AccessToken{ + { + Id: uuid.NewString(), + Permissions: []logs.PermissionsInner{ + logs.PERMISSIONSINNER_READ, + logs.PERMISSIONSINNER_WRITE, + }, + DisplayName: "Token", + AccessToken: utils.Ptr("Secret access token"), + Creator: uuid.NewString(), + Expires: false, + Status: logs.ACCESSTOKENSTATUS_ACTIVE, + }, + }, + }, + wantErr: false, + }, + { + name: "set empty access token", + args: args{ + accessTokens: []logs.AccessToken{ + {}, + }, + }, + wantErr: false, + }, + { + name: "empty", + args: args{}, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.accessTokens, tt.args.projectLabel); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/access_token/update/update.go b/internal/cmd/logs/access_token/update/update.go new file mode 100644 index 000000000..c265b639d --- /dev/null +++ b/internal/cmd/logs/access_token/update/update.go @@ -0,0 +1,145 @@ +package update + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +const ( + instanceIdFlag = "instance-id" + displayNameFlag = "display-name" + descriptionFlag = "description" + accessTokenIdArg = "ACCESS_TOKEN_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + AccessTokenId string + Description *string + DisplayName *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", accessTokenIdArg), + Short: "Updates a Logs access token", + Long: "Updates a access token.", + Args: args.SingleArg(accessTokenIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update access token with ID "xxx" with new name "access-token-1"`, + `$ stackit logs access-token update xxx --instance-id yyy --display-name access-token-1`, + ), + examples.NewExample( + `Update access token with ID "xxx" with new description "Access token for Service XY"`, + `$ stackit logs access-token update xxx --instance-id yyy --description "Access token for Service XY"`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get the display name for confirmation + instanceLabel, err := logUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get Logs instance: %v", err) + } + if instanceLabel == "" { + instanceLabel = model.InstanceId + } + + // Get the display name for confirmation + accessTokenLabel, err := logUtils.GetAccessTokenName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.AccessTokenId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get access token: %v", err) + } + if accessTokenLabel == "" { + accessTokenLabel = model.AccessTokenId + } + + prompt := fmt.Sprintf("Are you sure you want to update access token %q for instance %q?", accessTokenLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("update access token: %w", err) + } + + params.Printer.Outputf("Updated access token %q\n", accessTokenLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the Logs instance") + cmd.Flags().String(displayNameFlag, "", "Display name for the access token") + cmd.Flags().String(descriptionFlag, "", "Description of the access token") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + accessTokenId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + AccessTokenId: accessTokenId, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiUpdateAccessTokenRequest { + req := apiClient.DefaultAPI.UpdateAccessToken(ctx, model.ProjectId, model.Region, model.InstanceId, model.AccessTokenId) + + payload := logs.UpdateAccessTokenPayload{ + DisplayName: model.DisplayName, + Description: model.Description, + } + + return req.UpdateAccessTokenPayload(payload) +} diff --git a/internal/cmd/logs/access_token/update/update_test.go b/internal/cmd/logs/access_token/update/update_test.go new file mode 100644 index 000000000..3ee52f04a --- /dev/null +++ b/internal/cmd/logs/access_token/update/update_test.go @@ -0,0 +1,275 @@ +package update + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" + + testDisplayName = "display-name" + testDescription = "description" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testAccessTokenId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testAccessTokenId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + instanceIdFlag: testInstanceId, + displayNameFlag: testDisplayName, + descriptionFlag: testDescription, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + + InstanceId: testInstanceId, + AccessTokenId: testAccessTokenId, + DisplayName: utils.Ptr(testDisplayName), + Description: utils.Ptr(testDescription), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiUpdateAccessTokenRequest)) logs.ApiUpdateAccessTokenRequest { + request := testClient.DefaultAPI.UpdateAccessToken(testCtx, testProjectId, testRegion, testInstanceId, testAccessTokenId) + request = request.UpdateAccessTokenPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *logs.UpdateAccessTokenPayload)) logs.UpdateAccessTokenPayload { + payload := logs.UpdateAccessTokenPayload{ + DisplayName: utils.Ptr(testDisplayName), + Description: utils.Ptr(testDescription), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "required only", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.DisplayName = nil + model.Description = nil + }), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "access token id invalid 1", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "access token id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + model, err := parseInput(params.Printer, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiUpdateAccessTokenRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/logs/instance/create/create.go b/internal/cmd/logs/instance/create/create.go new file mode 100644 index 000000000..4e35772f5 --- /dev/null +++ b/internal/cmd/logs/instance/create/create.go @@ -0,0 +1,171 @@ +package create + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + + "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + displayNameFlag = "display-name" + retentionDaysFlag = "retention-days" + aclFlag = "acl" + descriptionFlag = "description" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + DisplayName *string + RetentionDays *int32 + ACL []string + Description *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a Logs instance", + Long: "Creates a Logs instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a Logs instance with name "my-instance" and retention time 10 days`, + `$ stackit logs instance create --display-name "my-instance" --retention-days 10`), + examples.NewExample( + `Create a Logs instance with name "my-instance", retention time 10 days, and a description`, + `$ stackit logs instance create --display-name "my-instance" --retention-days 10 --description "Description of the instance"`), + examples.NewExample( + `Create a Logs instance with name "my-instance", retention time 10 days, and restrict access to a specific range of IP addresses.`, + `$ stackit logs instance create --display-name "my-instance" --retention-days 10 --acl 1.2.3.0/24`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create a Logs instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create Logs instance: %w", err) + } + if resp == nil { + return fmt.Errorf("create Logs instance: empty response from API") + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateLogsInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resp.Id).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for logs instance creation: %w", err) + } + } + + return outputResult(params.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringSlice(aclFlag, []string{}, "Access control list") + cmd.Flags().Int32(retentionDaysFlag, 0, "The days for how long the logs should be stored before being cleaned up") + + err := flags.MarkFlagsRequired(cmd, displayNameFlag, retentionDaysFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + DisplayName: flags.FlagToStringPointer(p, cmd, displayNameFlag), + RetentionDays: flags.FlagToInt32Pointer(p, cmd, retentionDaysFlag), + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + ACL: flags.FlagToStringSliceValue(p, cmd, aclFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiCreateLogsInstanceRequest { + req := apiClient.DefaultAPI.CreateLogsInstance(ctx, model.ProjectId, model.Region) + req = req.CreateLogsInstancePayload(logs.CreateLogsInstancePayload{ + DisplayName: utils.PtrString(model.DisplayName), + Description: model.Description, + RetentionDays: utils.PtrValue(model.RetentionDays), + Acl: model.ACL, + }) + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp *logs.LogsInstance) error { + if resp == nil { + return fmt.Errorf("create logs instance response is empty") + } else if model == nil || model.GlobalFlagModel == nil { + return fmt.Errorf("input model is nil") + } + + return p.OutputResult(model.OutputFormat, resp, func() error { + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" + } + p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, resp.Id) + return nil + }) +} diff --git a/internal/cmd/logs/instance/create/create_test.go b/internal/cmd/logs/instance/create/create_test.go new file mode 100644 index 000000000..f2408a93f --- /dev/null +++ b/internal/cmd/logs/instance/create/create_test.go @@ -0,0 +1,258 @@ +package create + +import ( + "context" + "strconv" + "testing" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testRegion = "eu01" + testDisplayName = "my-logs-instance" + testDescription = "my instance description" + testAcl = "198.51.100.14/24" + testRetentionDays = 32 +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + testProjectId = uuid.NewString() +) + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: testDisplayName, + retentionDaysFlag: strconv.Itoa(testRetentionDays), + descriptionFlag: testDescription, + aclFlag: testAcl, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + DisplayName: utils.Ptr(testDisplayName), + Description: utils.Ptr(testDescription), + RetentionDays: utils.Ptr(int32(testRetentionDays)), + ACL: []string{testAcl}, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *logs.ApiCreateLogsInstanceRequest)) logs.ApiCreateLogsInstanceRequest { + request := testClient.DefaultAPI.CreateLogsInstance(testCtx, testProjectId, testRegion) + request = request.CreateLogsInstancePayload(logs.CreateLogsInstancePayload{ + DisplayName: testDisplayName, + Description: utils.Ptr(testDescription), + RetentionDays: testRetentionDays, + Acl: []string{testAcl}, + }) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "optional flags omitted", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + delete(flagValues, aclFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.ACL = nil + }), + }, + { + description: "no values provided", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "display name missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, displayNameFlag) + }), + isValid: false, + }, + { + description: "retention days missing (required)", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, retentionDaysFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiCreateLogsInstanceRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "no optional values", + model: fixtureInputModel(func(model *inputModel) { + model.Description = nil + model.ACL = nil + }), + expectedRequest: fixtureRequest().CreateLogsInstancePayload(logs.CreateLogsInstancePayload{ + DisplayName: testDisplayName, + RetentionDays: int32(testRetentionDays), + Description: nil, + Acl: nil, + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + diff := cmp.Diff(tt.expectedRequest, request, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + instance *logs.LogsInstance + wantErr bool + }{ + { + description: "nil response", + instance: nil, + wantErr: true, + }, + { + description: "model is nil", + instance: &logs.LogsInstance{}, + model: nil, + wantErr: true, + }, + { + description: "global flag nil", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: nil}, + wantErr: true, + }, + { + description: "default output", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wantErr: false, + }, + { + description: "json output", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wantErr: false, + }, + { + description: "yaml output", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, "label", tt.instance) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/instance/delete/delete.go b/internal/cmd/logs/instance/delete/delete.go new file mode 100644 index 000000000..8ffa95e44 --- /dev/null +++ b/internal/cmd/logs/instance/delete/delete.go @@ -0,0 +1,132 @@ +package delete + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" +) + +const ( + argInstanceID = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceID string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", argInstanceID), + Short: "Deletes the given Logs instance", + Long: "Deletes the given Logs instance.", + Args: args.SingleArg(argInstanceID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a Logs instance with ID "xxx"`, + `$ stackit logs instance delete "xxx"`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + instanceLabel, err := logsUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceID) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceID + } + + prompt := fmt.Sprintf("Are you sure you want to delete instance %q from project %q? (This cannot be undone)", instanceLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete Logs instance: %w", err) + } + + // Wait for async operation, if async mode not enabled + if !model.Async { + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteLogsInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceID).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for Logs instance deletion: %w", err) + } + } + + operationState := "Deleted" + if model.Async { + operationState = "Triggered deletion of" + } + params.Printer.Outputf("%s instance %q\n", operationState, instanceLabel) + return nil + }, + } + + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceID: instanceId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiDeleteLogsInstanceRequest { + req := apiClient.DefaultAPI.DeleteLogsInstance(ctx, model.ProjectId, model.Region, model.InstanceID) + return req +} diff --git a/internal/cmd/logs/instance/delete/delete_test.go b/internal/cmd/logs/instance/delete/delete_test.go new file mode 100644 index 000000000..162ae6055 --- /dev/null +++ b/internal/cmd/logs/instance/delete/delete_test.go @@ -0,0 +1,175 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu02" +) + +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +// Args +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +// Flags +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +// Input Model +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceID: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +// Request +func fixtureRequest(mods ...func(request *logs.ApiDeleteLogsInstanceRequest)) logs.ApiDeleteLogsInstanceRequest { + request := testClient.DefaultAPI.DeleteLogsInstance(testCtx, testProjectId, testRegion, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + expectedModel: fixtureInputModel(), + isValid: true, + }, + { + description: "no args (instanceID)", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiDeleteLogsInstanceRequest + }{ + { + description: "base case", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/logs/instance/describe/describe.go b/internal/cmd/logs/instance/describe/describe.go new file mode 100644 index 000000000..fa37d0c8e --- /dev/null +++ b/internal/cmd/logs/instance/describe/describe.go @@ -0,0 +1,120 @@ +package describe + +import ( + "context" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + argInstanceID = "INSTANCE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceID string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", argInstanceID), + Short: "Shows details of a Logs instance", + Long: "Shows details of a Logs instance", + Args: args.SingleArg(argInstanceID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Get details of a Logs instance with ID "xxx"`, + `$ stackit logs instance describe xxx`, + ), + examples.NewExample( + `Get details of a Logs instance with ID "xxx" in JSON format`, + "$ stackit logs instance describe xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get instance: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + model := &inputModel{ + GlobalFlagModel: globalFlags, + InstanceID: inputArgs[0], + } + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiGetLogsInstanceRequest { + return apiClient.DefaultAPI.GetLogsInstance(ctx, model.ProjectId, model.Region, model.InstanceID) +} + +func outputResult(p *print.Printer, outputFormat string, instance *logs.LogsInstance) error { + if instance == nil { + return fmt.Errorf("instance response is empty") + } + return p.OutputResult(outputFormat, instance, func() error { + table := tables.NewTable() + table.AddRow("ID", instance.Id) + table.AddSeparator() + table.AddRow("DISPLAY NAME", instance.DisplayName) + table.AddSeparator() + table.AddRow("RETENTION DAYS", instance.RetentionDays) + table.AddSeparator() + table.AddRow("ACL IP RANGES", instance.Acl) + table.AddSeparator() + table.AddRow("DATA SOURCE", utils.PtrString(instance.DatasourceUrl)) + table.AddSeparator() + table.AddRow("OTLP INGEST", utils.PtrString(instance.IngestOtlpUrl)) + table.AddSeparator() + table.AddRow("INGEST", utils.PtrString(instance.IngestUrl)) + table.AddSeparator() + table.AddRow("QUERY RANGE", utils.PtrString(instance.QueryRangeUrl)) + table.AddSeparator() + table.AddRow("QUERY", utils.PtrString(instance.QueryUrl)) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/logs/instance/describe/describe_test.go b/internal/cmd/logs/instance/describe/describe_test.go new file mode 100644 index 000000000..faca9f971 --- /dev/null +++ b/internal/cmd/logs/instance/describe/describe_test.go @@ -0,0 +1,192 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceID: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiGetLogsInstanceRequest)) logs.ApiGetLogsInstanceRequest { + request := testClient.DefaultAPI.GetLogsInstance(testCtx, testProjectId, testRegion, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "project id missing", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(m map[string]string) { delete(m, globalflags.ProjectIdFlag) }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: []string{testInstanceId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid instance id", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiGetLogsInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + instance *logs.LogsInstance + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default output", + args: args{outputFormat: "default", instance: &logs.LogsInstance{}}, + wantErr: false, + }, + { + name: "json output", + args: args{outputFormat: print.JSONOutputFormat, instance: &logs.LogsInstance{}}, + wantErr: false, + }, + { + name: "yaml output", + args: args{outputFormat: print.YAMLOutputFormat, instance: &logs.LogsInstance{}}, + wantErr: false, + }, + { + name: "nil instance", + args: args{instance: nil}, + wantErr: true, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/instance/instance.go b/internal/cmd/logs/instance/instance.go new file mode 100644 index 000000000..f69b7ddce --- /dev/null +++ b/internal/cmd/logs/instance/instance.go @@ -0,0 +1,34 @@ +package instance + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/instance/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/instance/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/instance/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/instance/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/instance/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "instance", + Short: "Provides functionality for Logs instances", + Long: "Provides functionality for Logs instances.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/logs/instance/list/list.go b/internal/cmd/logs/instance/list/list.go new file mode 100644 index 000000000..ac2d29497 --- /dev/null +++ b/internal/cmd/logs/instance/list/list.go @@ -0,0 +1,147 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +const ( + limitFlag = "limit" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists Logs instances", + Long: "Lists Logs instances within the project.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all Logs instances`, + `$ stackit logs instance list`, + ), + examples.NewExample( + `List the first 10 Logs instances`, + `$ stackit logs instance list --limit=10`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + // Call API + request := buildRequest(ctx, model, apiClient) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("list Logs instances: %w", err) + } + items := response.GetInstances() + + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiListLogsInstancesRequest { + request := apiClient.DefaultAPI.ListLogsInstances(ctx, model.ProjectId, model.Region) + + return request +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []logs.LogsInstance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No Logs instances found for project %q", projectLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("NAME", "ID", "STATUS") + for _, instance := range instances { + table.AddRow( + instance.DisplayName, + instance.Id, + instance.Status, + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/logs/instance/list/list_test.go b/internal/cmd/logs/instance/list/list_test.go new file mode 100644 index 000000000..339523562 --- /dev/null +++ b/internal/cmd/logs/instance/list/list_test.go @@ -0,0 +1,194 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiListLogsInstancesRequest)) logs.ApiListLogsInstancesRequest { + request := testClient.DefaultAPI.ListLogsInstances(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiListLogsInstancesRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + instances []logs.LogsInstance + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "empty instances slice", + args: args{ + instances: []logs.LogsInstance{}, + }, + wantErr: false, + }, + { + name: "empty instance in instances slice", + args: args{ + instances: []logs.LogsInstance{{}}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/instance/update/update.go b/internal/cmd/logs/instance/update/update.go new file mode 100644 index 000000000..6e89a266c --- /dev/null +++ b/internal/cmd/logs/instance/update/update.go @@ -0,0 +1,166 @@ +package update + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/client" + logsUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logs/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +const ( + argInstanceID = "INSTANCE_ID" + + displayNameFlag = "display-name" + retentionDaysFlag = "retention-days" + aclFlag = "acl" + descriptionFlag = "description" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceID string + DisplayName *string + RetentionDays *int32 + ACL []string + Description *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", argInstanceID), + Short: "Updates a Logs instance", + Long: "Updates a Logs instance.", + Args: args.SingleArg(argInstanceID, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Update the display name of the Logs instance with ID "xxx"`, + "$ stackit logs instance update xxx --display-name new-name"), + examples.NewExample( + `Update the retention time of the Logs instance with ID "xxx"`, + "$ stackit logs instance update xxx --retention-days 40"), + examples.NewExample( + `Update the ACL of the Logs instance with ID "xxx"`, + "$ stackit logs instance update xxx --acl 1.2.3.0/24"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + instanceLabel, err := logsUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceID) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceID + } + + prompt := fmt.Sprintf("Are you sure you want to update instance %s?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update logs instance: %w", err) + } + + return outputResult(params.Printer, model, projectLabel, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(displayNameFlag, "", "Display name") + cmd.Flags().String(descriptionFlag, "", "Description") + cmd.Flags().StringSlice(aclFlag, []string{}, "Access control list") + cmd.Flags().Int32(retentionDaysFlag, 0, "The days for how long the logs should be stored before being cleaned up") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + instanceId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + displayName := flags.FlagToStringPointer(p, cmd, displayNameFlag) + retentionDays := flags.FlagToInt32Pointer(p, cmd, retentionDaysFlag) + acl := flags.FlagToStringSliceValue(p, cmd, aclFlag) + description := flags.FlagToStringPointer(p, cmd, descriptionFlag) + + if displayName == nil && retentionDays == nil && acl == nil && description == nil { + return nil, &errors.EmptyUpdateError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceID: instanceId, + DisplayName: displayName, + ACL: acl, + Description: description, + RetentionDays: retentionDays, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *logs.APIClient) logs.ApiUpdateLogsInstanceRequest { + req := apiClient.DefaultAPI.UpdateLogsInstance(ctx, model.ProjectId, model.Region, model.InstanceID) + + req = req.UpdateLogsInstancePayload(logs.UpdateLogsInstancePayload{ + DisplayName: model.DisplayName, + Acl: model.ACL, + RetentionDays: model.RetentionDays, + Description: model.Description, + }) + return req +} + +func outputResult(p *print.Printer, model *inputModel, projectLabel string, instance *logs.LogsInstance) error { + if instance == nil { + return fmt.Errorf("instance is nil") + } else if model == nil || model.GlobalFlagModel == nil { + return fmt.Errorf("input model is nil") + } + return p.OutputResult(model.OutputFormat, instance, func() error { + p.Outputf("Updated instance %q for project %q.\n", instance.DisplayName, projectLabel) + return nil + }) +} diff --git a/internal/cmd/logs/instance/update/update_test.go b/internal/cmd/logs/instance/update/update_test.go new file mode 100644 index 000000000..c79ca18d3 --- /dev/null +++ b/internal/cmd/logs/instance/update/update_test.go @@ -0,0 +1,307 @@ +package update + +import ( + "context" + "testing" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" +) + +type testCtxKey struct{} + +const ( + testRegion = "eu01" +) + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &logs.APIClient{DefaultAPI: &logs.DefaultAPIService{}} + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testInstanceId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: "name", + aclFlag: "0.0.0.0/0", + retentionDaysFlag: "60", + descriptionFlag: "Example", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceID: testInstanceId, + DisplayName: utils.Ptr("name"), + ACL: []string{"0.0.0.0/0"}, + RetentionDays: utils.Ptr(int32(60)), + Description: utils.Ptr("Example"), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *logs.ApiUpdateLogsInstanceRequest)) logs.ApiUpdateLogsInstanceRequest { + request := testClient.DefaultAPI.UpdateLogsInstance(testCtx, testProjectId, testRegion, testInstanceId) + request = request.UpdateLogsInstancePayload(logs.UpdateLogsInstancePayload{ + DisplayName: utils.Ptr("name"), + Acl: []string{"0.0.0.0/0"}, + RetentionDays: utils.Ptr(int32(60)), + Description: utils.Ptr("Example"), + }) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + primaryFlagValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "required flags only (no values to update)", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + }, + isValid: false, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceID: testInstanceId, + }, + }, + { + description: "update all fields", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + displayNameFlag: "display-name", + aclFlag: "0.0.0.0/24", + descriptionFlag: "description", + retentionDaysFlag: "60", + }, + isValid: true, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceID: testInstanceId, + DisplayName: utils.Ptr("display-name"), + ACL: []string{"0.0.0.0/24"}, + RetentionDays: utils.Ptr(int32(60)), + Description: utils.Ptr("description"), + }, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest logs.ApiUpdateLogsInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "required fields only", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceID: testInstanceId, + }, + expectedRequest: testClient.DefaultAPI.UpdateLogsInstance(testCtx, testProjectId, testRegion, testInstanceId). + UpdateLogsInstancePayload(logs.UpdateLogsInstancePayload{}), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, logs.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + description string + model *inputModel + instance *logs.LogsInstance + wantErr bool + }{ + { + description: "nil response", + instance: nil, + wantErr: true, + }, + { + description: "default output", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, + wantErr: false, + }, + { + description: "model is nil", + instance: &logs.LogsInstance{}, + model: nil, + wantErr: true, + }, + { + description: "global flag nil", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: nil}, + wantErr: true, + }, + { + description: "json output", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.JSONOutputFormat}}, + wantErr: false, + }, + { + description: "yaml output", + instance: &logs.LogsInstance{}, + model: &inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{OutputFormat: print.YAMLOutputFormat}}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := outputResult(params.Printer, tt.model, "label", tt.instance) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/logs/logs.go b/internal/cmd/logs/logs.go new file mode 100644 index 000000000..e72cc5830 --- /dev/null +++ b/internal/cmd/logs/logs.go @@ -0,0 +1,28 @@ +package logs + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/access_token" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs/instance" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "logs", + Short: "Provides functionality for Logs", + Long: "Provides functionality for Logs.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(instance.NewCmd(params)) + cmd.AddCommand(access_token.NewCmd(params)) +} diff --git a/internal/cmd/mariadb/credentials/create/create.go b/internal/cmd/mariadb/credentials/create/create.go index cf42e1ca2..f81cb41cc 100644 --- a/internal/cmd/mariadb/credentials/create/create.go +++ b/internal/cmd/mariadb/credentials/create/create.go @@ -2,11 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,10 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/mariadb/client" mariadbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mariadb/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" ) const ( @@ -32,7 +30,7 @@ type inputModel struct { ShowPassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for a MariaDB instance", @@ -46,9 +44,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for a MariaDB instance and show the password in the output`, "$ stackit mariadb credentials create --instance-id xxx --show-password"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -59,18 +57,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -95,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -107,20 +103,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ShowPassword: flags.FlagToBoolValue(p, cmd, showPasswordFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.InstanceId) return req } @@ -129,42 +117,26 @@ func outputResult(p *print.Printer, outputFormat string, showPassword bool, inst return fmt.Errorf("response is nil") } - if !showPassword && resp.HasRaw() && resp.Raw.Credentials != nil { - resp.Raw.Credentials.Password = utils.Ptr("hidden") + if !showPassword && resp.HasRaw() { + resp.Raw.Credentials.Password = "hidden" } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB credentials list: %w", err) - } - p.Outputln(string(details)) - return nil - default: - p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, utils.PtrString(resp.Id)) + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, resp.Id) // The username field cannot be set by the user, so we only display it if it's not returned empty - if resp.HasRaw() && resp.Raw.Credentials != nil { - if username := resp.Raw.Credentials.Username; username != nil && *username != "" { - p.Outputf("Username: %s\n", *username) + if resp.HasRaw() { + if resp.Raw.Credentials.Username != "" { + p.Outputf("Username: %s\n", resp.Raw.Credentials.Username) } if !showPassword { p.Outputf("Password: \n") } else { - p.Outputf("Password: %s\n", utils.PtrString(resp.Raw.Credentials.Password)) + p.Outputf("Password: %s\n", resp.Raw.Credentials.Password) } - p.Outputf("Host: %s\n", utils.PtrString(resp.Raw.Credentials.Host)) - p.Outputf("Port: %s\n", utils.PtrString(resp.Raw.Credentials.Port)) + p.Outputf("Host: %s\n", resp.Raw.Credentials.Host) + p.Outputf("Port: %d\n", resp.Raw.Credentials.Port) } - p.Outputf("URI: %s\n", utils.PtrString(resp.Uri)) + p.Outputf("URI: %s\n", resp.Uri) return nil - } + }) } diff --git a/internal/cmd/mariadb/credentials/create/create_test.go b/internal/cmd/mariadb/credentials/create/create_test.go index 5516a2415..40b423653 100644 --- a/internal/cmd/mariadb/credentials/create/create_test.go +++ b/internal/cmd/mariadb/credentials/create/create_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -49,7 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiCreateCredentialsRequest)) mariadb.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -59,6 +59,7 @@ func fixtureRequest(mods ...func(request *mariadb.ApiCreateCredentialsRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -130,46 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -193,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -228,11 +190,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.showPassword, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.showPassword, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mariadb/credentials/credentials.go b/internal/cmd/mariadb/credentials/credentials.go index e23c3887f..7f216ad4b 100644 --- a/internal/cmd/mariadb/credentials/credentials.go +++ b/internal/cmd/mariadb/credentials/credentials.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/credentials/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for MariaDB credentials", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/mariadb/credentials/delete/delete.go b/internal/cmd/mariadb/credentials/delete/delete.go index cbce0078d..f77d558cc 100644 --- a/internal/cmd/mariadb/credentials/delete/delete.go +++ b/internal/cmd/mariadb/credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsIdArg), Short: "Deletes credentials of a MariaDB instance", @@ -55,24 +56,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - credentialsLabel, err := mariadbUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialsLabel, err := mariadbUtils.GetCredentialsUsername(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.CredentialsId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials username: %v", err) credentialsLabel = model.CredentialsId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,19 +110,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) return req } diff --git a/internal/cmd/mariadb/credentials/delete/delete_test.go b/internal/cmd/mariadb/credentials/delete/delete_test.go index 81da9d7b3..c192ad5e1 100644 --- a/internal/cmd/mariadb/credentials/delete/delete_test.go +++ b/internal/cmd/mariadb/credentials/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() @@ -61,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiDeleteCredentialsRequest)) mariadb.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -165,54 +164,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +188,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mariadb/credentials/describe/describe.go b/internal/cmd/mariadb/credentials/describe/describe.go index 7a03a4377..e2f9f5f7f 100644 --- a/internal/cmd/mariadb/credentials/describe/describe.go +++ b/internal/cmd/mariadb/credentials/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialsIdArg), Short: "Shows details of credentials of a MariaDB instance", @@ -95,20 +94,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiGetCredentialsRequest { - req := apiClient.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) return req } @@ -117,34 +108,17 @@ func outputResult(p *print.Printer, outputFormat string, credentials *mariadb.Cr return fmt.Errorf("credentials is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, credentials, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(credentials.Id)) + table.AddRow("ID", credentials.Id) table.AddSeparator() // The username field cannot be set by the user so we only display it if it's not returned empty - if credentials.HasRaw() && credentials.Raw.Credentials != nil { - if username := credentials.Raw.Credentials.Username; username != nil && *username != "" { - table.AddRow("USERNAME", *username) + if credentials.HasRaw() { + if credentials.Raw.Credentials.Username != "" { + table.AddRow("USERNAME", credentials.Raw.Credentials.Username) table.AddSeparator() } - table.AddRow("PASSWORD", utils.PtrString(credentials.Raw.Credentials.Password)) + table.AddRow("PASSWORD", credentials.Raw.Credentials.Password) table.AddSeparator() table.AddRow("URI", utils.PtrString(credentials.Raw.Credentials.Uri)) } @@ -154,5 +128,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials *mariadb.Cr } return nil - } + }) } diff --git a/internal/cmd/mariadb/credentials/describe/describe_test.go b/internal/cmd/mariadb/credentials/describe/describe_test.go index 522049aec..d677079c4 100644 --- a/internal/cmd/mariadb/credentials/describe/describe_test.go +++ b/internal/cmd/mariadb/credentials/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() @@ -61,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiGetCredentialsRequest)) mariadb.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -165,54 +165,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +189,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -269,11 +222,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mariadb/credentials/list/list.go b/internal/cmd/mariadb/credentials/list/list.go index f9a3f43d7..90f42ad12 100644 --- a/internal/cmd/mariadb/credentials/list/list.go +++ b/internal/cmd/mariadb/credentials/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/mariadb/client" mariadbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mariadb/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials' IDs for a MariaDB instance", @@ -49,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials' IDs for a MariaDB instance`, "$ stackit mariadb credentials list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,22 +67,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list MariaDB credentials: %w", err) } - credentials := *resp.CredentialsList - if len(credentials) == 0 { - instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId - } - params.Printer.Info("No credentials found for instance %q\n", instanceLabel) - return nil + credentials := resp.GetCredentialsList() + + instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, credentials) }, } configureFlags(cmd) @@ -98,7 +94,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,47 +114,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []mariadb.CredentialsListItem) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, credentials []mariadb.CredentialsListItem) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID") for i := range credentials { c := credentials[i] - table.AddRow(utils.PtrString(c.Id)) + table.AddRow(c.Id) } err := table.Display(p) if err != nil { @@ -166,5 +142,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials []mariadb.C } return nil - } + }) } diff --git a/internal/cmd/mariadb/credentials/list/list_test.go b/internal/cmd/mariadb/credentials/list/list_test.go index c4fbce7b2..8b4f02f23 100644 --- a/internal/cmd/mariadb/credentials/list/list_test.go +++ b/internal/cmd/mariadb/credentials/list/list_test.go @@ -4,31 +4,29 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -52,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiListCredentialsRequest)) mariadb.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -62,6 +60,7 @@ func fixtureRequest(mods ...func(request *mariadb.ApiListCredentialsRequest)) ma func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -80,21 +79,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,46 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +160,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -211,8 +171,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - credentials []mariadb.CredentialsListItem + outputFormat string + instanceLabel string + credentials []mariadb.CredentialsListItem } tests := []struct { name string @@ -240,11 +201,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mariadb/instance/create/create.go b/internal/cmd/mariadb/instance/create/create.go index b84368fa2..5dad458e1 100644 --- a/internal/cmd/mariadb/instance/create/create.go +++ b/internal/cmd/mariadb/instance/create/create.go @@ -2,13 +2,12 @@ package create import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +21,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api/wait" ) const ( @@ -48,15 +47,15 @@ type inputModel struct { InstanceName *string EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a MariaDB instance", @@ -73,9 +72,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a MariaDB instance with name "my-instance" and specify IP range which is allowed to access it`, "$ stackit mariadb instance create --name my-instance --plan-id xxx --acl 1.2.3.0/24"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -92,16 +91,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a MariaDB instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a MariaDB instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -113,17 +110,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create MariaDB instance: %w", err) } - instanceId := *resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, resp.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MariaDB instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) @@ -137,7 +133,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") @@ -150,7 +146,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -177,39 +173,26 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { EnableMonitoring: flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag), MonitoringInstanceId: flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag), Graphite: flags.FlagToStringPointer(p, cmd, graphiteFlag), - MetricsFrequency: flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag), + MetricsFrequency: flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag), MetricsPrefix: flags.FlagToStringPointer(p, cmd, metricsPrefixFlag), SgwAcl: flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag), - Syslog: flags.FlagToStringSlicePointer(p, cmd, syslogFlag), + Syslog: flags.FlagToStringSliceValue(p, cmd, syslogFlag), PlanId: planId, PlanName: planName, Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type mariaDBClient interface { - CreateInstance(ctx context.Context, projectId string) mariadb.ApiCreateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*mariadb.ListOfferingsResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient mariaDBClient) (mariadb.ApiCreateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient mariadb.DefaultAPI) (mariadb.ApiCreateInstanceRequest, error) { req := apiClient.CreateInstance(ctx, model.ProjectId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get MariaDB offerings: %w", err) } @@ -230,14 +213,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient mariaDBClien } planId = model.PlanId } - var sgwAcl *string if model.SgwAcl != nil { sgwAcl = utils.Ptr(strings.Join(*model.SgwAcl, ",")) } req = req.CreateInstancePayload(mariadb.CreateInstancePayload{ - InstanceName: model.InstanceName, + InstanceName: utils.PtrString(model.InstanceName), Parameters: &mariadb.InstanceParameters{ EnableMonitoring: model.EnableMonitoring, Graphite: model.Graphite, @@ -247,7 +229,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient mariaDBClien SgwAcl: sgwAcl, Syslog: model.Syslog, }, - PlanId: planId, + PlanId: utils.PtrString(planId), }) return req, nil } @@ -257,29 +239,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe return fmt.Errorf("response is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } - p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, utils.PtrString(resp.InstanceId)) + p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, resp.InstanceId) return nil - } + }) } diff --git a/internal/cmd/mariadb/instance/create/create_test.go b/internal/cmd/mariadb/instance/create/create_test.go index 33dedf860..e338f7cc4 100644 --- a/internal/cmd/mariadb/instance/create/create_test.go +++ b/internal/cmd/mariadb/instance/create/create_test.go @@ -5,15 +5,15 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,22 +21,22 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} -type mariaDBClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *mariadb.ListOfferingsResponse } -func (c *mariaDBClientMocked) CreateInstance(ctx context.Context, projectId string) mariadb.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId) -} - -func (c *mariaDBClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*mariadb.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) mariadb.DefaultAPI { + return &mariadb.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ mariadb.ApiListOfferingsRequest) (*mariadb.ListOfferingsResponse, error) { + if s.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return s.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var testProjectId = uuid.NewString() @@ -71,11 +71,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { InstanceName: utils.Ptr("example-name"), EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -85,19 +85,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiCreateInstanceRequest)) mariadb.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId) request = request.CreateInstancePayload(mariadb.CreateInstancePayload{ - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", Parameters: &mariadb.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }) for _, mod := range mods { mod(&request) @@ -108,6 +108,7 @@ func fixtureRequest(mods ...func(request *mariadb.ApiCreateInstanceRequest)) mar func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string sgwAclValues []string syslogValues []string @@ -177,7 +178,7 @@ func TestParseInput(t *testing.T) { InstanceName: utils.Ptr(""), EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -253,75 +254,17 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + syslogFlag: tt.syslogValues, + }, tt.isValid) }) } } @@ -340,13 +283,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), getOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -364,13 +307,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), getOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -399,13 +342,13 @@ func TestBuildRequest(t *testing.T) { }, ), getOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -423,30 +366,30 @@ func TestBuildRequest(t *testing.T) { PlanId: utils.Ptr(testPlanId), }, getOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, }, }, - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(mariadb.CreateInstancePayload{PlanId: utils.Ptr(testPlanId), Parameters: &mariadb.InstanceParameters{}}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId). + CreateInstancePayload(mariadb.CreateInstancePayload{PlanId: testPlanId, Parameters: &mariadb.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mariaDBClientMocked{ + settings := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.getOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(settings)) if err != nil { if !tt.isValid { return @@ -457,6 +400,8 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateEmpty(), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -491,11 +436,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mariadb/instance/delete/delete.go b/internal/cmd/mariadb/instance/delete/delete.go index eaca7989a..c0c5e1cc2 100644 --- a/internal/cmd/mariadb/instance/delete/delete.go +++ b/internal/cmd/mariadb/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a MariaDB instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MariaDB instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.InstanceId) return req } diff --git a/internal/cmd/mariadb/instance/delete/delete_test.go b/internal/cmd/mariadb/instance/delete/delete_test.go index 737f2affc..f4817c48e 100644 --- a/internal/cmd/mariadb/instance/delete/delete_test.go +++ b/internal/cmd/mariadb/instance/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiDeleteInstanceRequest)) mariadb.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +161,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mariadb/instance/describe/describe.go b/internal/cmd/mariadb/instance/describe/describe.go index 58f07db66..4ce4c19a6 100644 --- a/internal/cmd/mariadb/instance/describe/describe.go +++ b/internal/cmd/mariadb/instance/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a MariaDB instance", @@ -83,20 +82,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId) return req } @@ -105,39 +96,22 @@ func outputResult(p *print.Printer, outputFormat string, instance *mariadb.Insta return fmt.Errorf("instance is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(instance.InstanceId)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - if instance.LastOperation != nil { - table.AddRow("LAST OPERATION TYPE", utils.PtrString(instance.LastOperation.Type)) + if _, ok := instance.LastOperation.GetStateOk(); ok { + table.AddRow("LAST OPERATION TYPE", instance.LastOperation.GetType()) table.AddSeparator() - table.AddRow("LAST OPERATION STATE", utils.PtrString(instance.LastOperation.State)) + table.AddRow("LAST OPERATION STATE", instance.LastOperation.GetState()) table.AddSeparator() } - table.AddRow("PLAN ID", utils.PtrString(instance.PlanId)) + table.AddRow("PLAN ID", instance.PlanId) // Only show ACL if it's present and not empty if instance.Parameters != nil { - acl := (*instance.Parameters)[aclParameterKey] + acl := instance.Parameters[aclParameterKey] aclStr, ok := acl.(string) if ok { if aclStr != "" { @@ -152,5 +126,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *mariadb.Insta } return nil - } + }) } diff --git a/internal/cmd/mariadb/instance/describe/describe_test.go b/internal/cmd/mariadb/instance/describe/describe_test.go index 3d5cc03ec..5f16c5344 100644 --- a/internal/cmd/mariadb/instance/describe/describe_test.go +++ b/internal/cmd/mariadb/instance/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiGetInstanceRequest)) mariadb.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -242,11 +195,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mariadb/instance/instance.go b/internal/cmd/mariadb/instance/instance.go index 3b16f4864..e46e875f8 100644 --- a/internal/cmd/mariadb/instance/instance.go +++ b/internal/cmd/mariadb/instance/instance.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/instance/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for MariaDB instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/mariadb/instance/list/list.go b/internal/cmd/mariadb/instance/list/list.go index 7e5b8e0d6..f8da15fb2 100644 --- a/internal/cmd/mariadb/instance/list/list.go +++ b/internal/cmd/mariadb/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/mariadb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all MariaDB instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 MariaDB instances`, "$ stackit mariadb instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +66,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get MariaDB instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + instances := resp.GetInstances() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,56 +110,33 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, instances []mariadb.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []mariadb.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "LAST OPERATION TYPE", "LAST OPERATION STATE") for i := range instances { instance := instances[i] - lastOperationType, lastOperationState := "", "" - if instance.LastOperation != nil { - lastOperationType = utils.PtrString(instance.LastOperation.Type) - lastOperationState = utils.PtrString(instance.LastOperation.State) - } + lastOperationType := string(instance.LastOperation.GetType()) + lastOperationState := string(instance.LastOperation.GetState()) table.AddRow( utils.PtrString(instance.InstanceId), - utils.PtrString(instance.Name), + instance.Name, lastOperationType, lastOperationState, ) @@ -173,5 +147,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []mariadb.Ins } return nil - } + }) } diff --git a/internal/cmd/mariadb/instance/list/list_test.go b/internal/cmd/mariadb/instance/list/list_test.go index 7f199d16c..23f432c79 100644 --- a/internal/cmd/mariadb/instance/list/list_test.go +++ b/internal/cmd/mariadb/instance/list/list_test.go @@ -4,30 +4,27 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -50,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiListInstancesRequest)) mariadb.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +57,7 @@ func fixtureRequest(mods ...func(request *mariadb.ApiListInstancesRequest)) mari func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +76,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -114,48 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +136,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +148,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []mariadb.Instance } tests := []struct { @@ -219,11 +177,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mariadb/instance/update/update.go b/internal/cmd/mariadb/instance/update/update.go index 26059ff6f..add9a31cd 100644 --- a/internal/cmd/mariadb/instance/update/update.go +++ b/internal/cmd/mariadb/instance/update/update.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api/wait" ) const ( @@ -46,15 +47,15 @@ type inputModel struct { EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a MariaDB instance", @@ -81,22 +82,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := mariadbUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -112,13 +111,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MariaDB instance update: %w", err) } - s.Stop() } operationState := "Updated" @@ -136,7 +135,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") @@ -157,10 +156,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu enableMonitoring := flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag) monitoringInstanceId := flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag) graphite := flags.FlagToStringPointer(p, cmd, graphiteFlag) - metricsFrequency := flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag) + metricsFrequency := flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag) metricsPrefix := flags.FlagToStringPointer(p, cmd, metricsPrefixFlag) sgwAcl := flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag) - syslog := flags.FlagToStringSlicePointer(p, cmd, syslogFlag) + syslog := flags.FlagToStringSliceValue(p, cmd, syslogFlag) planId := flags.FlagToStringPointer(p, cmd, planIdFlag) planName := flags.FlagToStringValue(p, cmd, planNameFlag) version := flags.FlagToStringValue(p, cmd, versionFlag) @@ -194,30 +193,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type mariaDBClient interface { - PartialUpdateInstance(ctx context.Context, projectId, instanceId string) mariadb.ApiPartialUpdateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*mariadb.ListOfferingsResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient mariaDBClient) (mariadb.ApiPartialUpdateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient mariadb.DefaultAPI) (mariadb.ApiPartialUpdateInstanceRequest, error) { req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get MariaDB offerings: %w", err) } diff --git a/internal/cmd/mariadb/instance/update/update_test.go b/internal/cmd/mariadb/instance/update/update_test.go index 5fb7369c5..fdcefd87b 100644 --- a/internal/cmd/mariadb/instance/update/update_test.go +++ b/internal/cmd/mariadb/instance/update/update_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,22 +20,22 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} -type mariaDBClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *mariadb.ListOfferingsResponse } -func (c *mariaDBClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId string) mariadb.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId) -} - -func (c *mariaDBClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*mariadb.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) mariadb.DefaultAPI { + return &mariadb.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ mariadb.ApiListOfferingsRequest) (*mariadb.ListOfferingsResponse, error) { + if s.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return s.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var ( @@ -83,11 +82,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { InstanceId: testInstanceId, EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -97,16 +96,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiPartialUpdateInstanceRequest)) mariadb.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) request = request.PartialUpdateInstancePayload(mariadb.PartialUpdateInstancePayload{ Parameters: &mariadb.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, PlanId: utils.Ptr(testPlanId), }) @@ -187,7 +186,7 @@ func TestParseInput(t *testing.T) { PlanId: utils.Ptr(testPlanId), EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -269,17 +268,15 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -331,7 +328,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -364,13 +361,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), listOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -388,13 +385,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), listOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -423,13 +420,13 @@ func TestBuildRequest(t *testing.T) { }, ), listOfferingsResp: &mariadb.ListOfferingsResponse{ - Offerings: &[]mariadb.Offering{ + Offerings: []mariadb.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]mariadb.Plan{ + Version: "example-version", + Plans: []mariadb.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -446,18 +443,18 @@ func TestBuildRequest(t *testing.T) { }, InstanceId: testInstanceId, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). PartialUpdateInstancePayload(mariadb.PartialUpdateInstancePayload{Parameters: &mariadb.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mariaDBClientMocked{ + settings := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.listOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(settings)) if err != nil { if !tt.isValid { return @@ -468,6 +465,8 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateEmpty(), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mariadb/mariadb.go b/internal/cmd/mariadb/mariadb.go index 602949253..5f8c41185 100644 --- a/internal/cmd/mariadb/mariadb.go +++ b/internal/cmd/mariadb/mariadb.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/credentials" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb/plans" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "mariadb", Short: "Provides functionality for MariaDB", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(plans.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) diff --git a/internal/cmd/mariadb/plans/plans.go b/internal/cmd/mariadb/plans/plans.go index c54703a1e..8f7c7d4d5 100644 --- a/internal/cmd/mariadb/plans/plans.go +++ b/internal/cmd/mariadb/plans/plans.go @@ -2,12 +2,13 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/mariadb/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists all MariaDB service plans", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 MariaDB service plans`, "$ stackit mariadb plans --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +65,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get MariaDB service plans: %w", err) } - plans := *resp.Offerings - if len(plans) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No plans found for project %q\n", projectLabel) - return nil + plans := resp.GetOfferings() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +78,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { plans = plans[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, plans) + return outputResult(params.Printer, model.OutputFormat, projectLabel, plans) }, } @@ -94,7 +90,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,55 +109,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.APIClient) mariadb.ApiListOfferingsRequest { - req := apiClient.ListOfferings(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListOfferings(ctx, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, plans []mariadb.Offering) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("marshal MariaDB plans: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MariaDB plans: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []mariadb.Offering) error { + return p.OutputResult(outputFormat, plans, func() error { + if len(plans) == 0 { + p.Outputf("No plans found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] if o.Plans != nil { - for j := range *o.Plans { - plan := (*o.Plans)[j] + for j := range o.Plans { + plan := o.Plans[j] table.AddRow( - utils.PtrString(o.Name), - utils.PtrString(o.Version), - utils.PtrString(plan.Id), - utils.PtrString(plan.Name), - utils.PtrString(plan.Description), + o.Name, + o.Version, + plan.Id, + plan.Name, + plan.Description, ) } table.AddSeparator() @@ -174,5 +150,5 @@ func outputResult(p *print.Printer, outputFormat string, plans []mariadb.Offerin } return nil - } + }) } diff --git a/internal/cmd/mariadb/plans/plans_test.go b/internal/cmd/mariadb/plans/plans_test.go index f80f6c9cd..a99ae091a 100644 --- a/internal/cmd/mariadb/plans/plans_test.go +++ b/internal/cmd/mariadb/plans/plans_test.go @@ -4,30 +4,27 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mariadb.APIClient{} +var testClient = &mariadb.APIClient{DefaultAPI: &mariadb.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -50,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mariadb.ApiListOfferingsRequest)) mariadb.ApiListOfferingsRequest { - request := testClient.ListOfferings(testCtx, testProjectId) + request := testClient.DefaultAPI.ListOfferings(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +57,7 @@ func fixtureRequest(mods ...func(request *mariadb.ApiListOfferingsRequest)) mari func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +76,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -114,48 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +136,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mariadb.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +148,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string plans []mariadb.Offering } tests := []struct { @@ -219,11 +177,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.plans); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/backup/backup.go b/internal/cmd/mongodbflex/backup/backup.go index af716446b..e9b3e79d1 100644 --- a/internal/cmd/mongodbflex/backup/backup.go +++ b/internal/cmd/mongodbflex/backup/backup.go @@ -7,14 +7,14 @@ import ( restorejobs "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore-jobs" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/schedule" updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/update-schedule" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "backup", Short: "Provides functionality for MongoDB Flex instance backups", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(updateschedule.NewCmd(params)) cmd.AddCommand(schedule.NewCmd(params)) cmd.AddCommand(restore.NewCmd(params)) diff --git a/internal/cmd/mongodbflex/backup/describe/describe.go b/internal/cmd/mongodbflex/backup/describe/describe.go index a1ca3aeaf..5897bc04f 100644 --- a/internal/cmd/mongodbflex/backup/describe/describe.go +++ b/internal/cmd/mongodbflex/backup/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( mongoUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( @@ -34,7 +34,7 @@ type inputModel struct { BackupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", backupIdArg), Short: "Shows details of a backup for a MongoDB Flex instance", @@ -61,7 +61,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongoUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongoUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId @@ -75,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("describe backup for MongoDB Flex instance: %w", err) } - restoreJobs, err := apiClient.ListRestoreJobs(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() + restoreJobs, err := apiClient.DefaultAPI.ListRestoreJobs(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() if err != nil { return fmt.Errorf("get restore jobs for MongoDB Flex instance %q: %w", instanceLabel, err) } restoreJobState := mongoUtils.GetRestoreStatus(model.BackupId, restoreJobs) - return outputResult(params.Printer, model.OutputFormat, restoreJobState, *resp.Item) + return outputResult(params.Printer, model.OutputFormat, restoreJobState, resp.Item) }, } configureFlags(cmd) @@ -109,42 +109,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupId: backupId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetBackupRequest { - req := apiClient.GetBackup(ctx, model.ProjectId, model.InstanceId, model.BackupId, model.Region) + req := apiClient.DefaultAPI.GetBackup(ctx, model.ProjectId, model.InstanceId, model.BackupId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat, restoreStatus string, backup mongodbflex.Backup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backup, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex backup: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex backup: %w", err) +func outputResult(p *print.Printer, outputFormat, restoreStatus string, backup *mongodbflex.Backup) error { + return p.OutputResult(outputFormat, backup, func() error { + if backup == nil { + return fmt.Errorf("API response is nil") } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.AddRow("ID", utils.PtrString(backup.Id)) table.AddSeparator() @@ -163,5 +141,5 @@ func outputResult(p *print.Printer, outputFormat, restoreStatus string, backup m } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/backup/describe/describe_test.go b/internal/cmd/mongodbflex/backup/describe/describe_test.go index f69454e9b..cb173a533 100644 --- a/internal/cmd/mongodbflex/backup/describe/describe_test.go +++ b/internal/cmd/mongodbflex/backup/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -65,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiGetBackupRequest)) mongodbflex.ApiGetBackupRequest { - request := testClient.GetBackup(testCtx, testProjectId, testInstanceId, testBackupId, testRegion) + request := testClient.DefaultAPI.GetBackup(testCtx, testProjectId, testInstanceId, testBackupId, testRegion) for _, mod := range mods { mod(&request) } @@ -163,54 +163,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -234,7 +187,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -246,7 +199,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string - backup mongodbflex.Backup + backup *mongodbflex.Backup restoreStatus string } tests := []struct { @@ -257,21 +210,20 @@ func TestOutputResult(t *testing.T) { { name: "empty", args: args{}, - wantErr: false, + wantErr: true, }, { name: "set backup", args: args{ - backup: mongodbflex.Backup{}, + backup: &mongodbflex.Backup{}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.restoreStatus, tt.args.backup); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.restoreStatus, tt.args.backup); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/backup/list/list.go b/internal/cmd/mongodbflex/backup/list/list.go index 64187cf64..6678b3f9e 100644 --- a/internal/cmd/mongodbflex/backup/list/list.go +++ b/internal/cmd/mongodbflex/backup/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all backups which are available for a MongoDB Flex instance", @@ -51,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit mongodbflex backup list --instance-id xxx --limit 10"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,7 +63,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, *model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = *model.InstanceId @@ -76,13 +75,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get backups for MongoDB Flex instance %q: %w", instanceLabel, err) } - if resp.Items == nil || len(*resp.Items) == 0 { - cmd.Printf("No backups found for instance %q\n", instanceLabel) - return nil - } - backups := *resp.Items + backups := resp.Items - restoreJobs, err := apiClient.ListRestoreJobs(ctx, model.ProjectId, *model.InstanceId, model.Region).Execute() + restoreJobs, err := apiClient.DefaultAPI.ListRestoreJobs(ctx, model.ProjectId, *model.InstanceId, model.Region).Execute() if err != nil { return fmt.Errorf("get restore jobs for MongoDB Flex instance %q: %w", instanceLabel, err) } @@ -92,7 +87,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { backups = backups[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, backups, restoreJobs) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, backups, restoreJobs) }, } @@ -108,7 +103,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -128,46 +123,26 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiListBackupsRequest { - req := apiClient.ListBackups(ctx, model.ProjectId, *model.InstanceId, model.Region) + req := apiClient.DefaultAPI.ListBackups(ctx, model.ProjectId, *model.InstanceId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, backups []mongodbflex.Backup, restoreJobs *mongodbflex.ListRestoreJobsResponse) error { +func outputResult(p *print.Printer, outputFormat, instanceLabel string, backups []mongodbflex.Backup, restoreJobs *mongodbflex.ListRestoreJobsResponse) error { if restoreJobs == nil { return fmt.Errorf("restore jobs is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backups, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex backups list: %w", err) + return p.OutputResult(outputFormat, backups, func() error { + if len(backups) == 0 { + p.Outputf("No backups found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex backups list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "CREATED AT", "EXPIRES AT", "BACKUP SIZE", "RESTORE STATUS") for i := range backups { @@ -187,5 +162,5 @@ func outputResult(p *print.Printer, outputFormat string, backups []mongodbflex.B } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/backup/list/list_test.go b/internal/cmd/mongodbflex/backup/list/list_test.go index 8b6ce7a25..92b4c8e82 100644 --- a/internal/cmd/mongodbflex/backup/list/list_test.go +++ b/internal/cmd/mongodbflex/backup/list/list_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -56,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiListBackupsRequest)) mongodbflex.ApiListBackupsRequest { - request := testClient.ListBackups(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.ListBackups(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -66,6 +66,7 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiListBackupsRequest)) mo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -141,46 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -215,9 +177,10 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - backups []mongodbflex.Backup - restoreJobs *mongodbflex.ListRestoreJobsResponse + outputFormat string + instanceLabel string + backups []mongodbflex.Backup + restoreJobs *mongodbflex.ListRestoreJobsResponse } tests := []struct { name string @@ -252,11 +215,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backups, tt.args.restoreJobs); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.backups, tt.args.restoreJobs); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go index 7f529891e..5d0968f2c 100644 --- a/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go +++ b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go @@ -2,12 +2,13 @@ package restorejobs import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( mongodbflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "restore-jobs", Short: "Lists all restore jobs which have been run for a MongoDB Flex instance", @@ -50,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit mongodbflex backup restore-jobs --instance-id xxx --limit 10"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -63,7 +63,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, *model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = *model.InstanceId @@ -75,18 +75,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get restore jobs for MongoDB Flex instance %q: %w", instanceLabel, err) } - if resp.Items == nil || len(*resp.Items) == 0 { - cmd.Printf("No restore jobs found for instance %q\n", instanceLabel) - return nil - } - restoreJobs := *resp.Items + + restoreJobs := resp.GetItems() // Truncate output if model.Limit != nil && len(restoreJobs) > int(*model.Limit) { restoreJobs = restoreJobs[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, restoreJobs) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, restoreJobs) }, } @@ -102,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -122,42 +119,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiListRestoreJobsRequest { - req := apiClient.ListRestoreJobs(ctx, model.ProjectId, *model.InstanceId, model.Region) + req := apiClient.DefaultAPI.ListRestoreJobs(ctx, model.ProjectId, *model.InstanceId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, restoreJobs []mongodbflex.RestoreInstanceStatus) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(restoreJobs, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex restore jobs list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(restoreJobs, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex restore jobs list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, restoreJobs []mongodbflex.RestoreInstanceStatus) error { + return p.OutputResult(outputFormat, restoreJobs, func() error { + if len(restoreJobs) == 0 { + p.Outputf("No restore jobs found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "BACKUP ID", "BACKUP INSTANCE ID", "DATE", "STATUS") for i := range restoreJobs { @@ -177,5 +153,5 @@ func outputResult(p *print.Printer, outputFormat string, restoreJobs []mongodbfl } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go index fd00003a4..c1f7ec279 100644 --- a/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go +++ b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -56,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiListRestoreJobsRequest)) mongodbflex.ApiListRestoreJobsRequest { - request := testClient.ListRestoreJobs(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.ListRestoreJobs(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -66,6 +66,7 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiListRestoreJobsRequest) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -141,46 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -215,8 +177,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - restoreJobs []mongodbflex.RestoreInstanceStatus + outputFormat string + instanceLabel string + restoreJobs []mongodbflex.RestoreInstanceStatus } tests := []struct { name string @@ -243,11 +206,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.restoreJobs); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.restoreJobs); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go index 5c64f6aef..9c6768652 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore.go +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api/wait" ) const ( @@ -35,7 +36,7 @@ type inputModel struct { Timestamp string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "restore", Short: "Restores a MongoDB Flex instance from a backup", @@ -56,10 +57,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Restore a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx"`, `$ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id xxx`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,18 +71,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to restore MongoDB Flex instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to restore MongoDB Flex instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // If backupInstanceId is not provided, the target is the same instance as the backup @@ -100,16 +99,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } if !model.Async { - s := spinner.New(params.Printer) - s.Start("Restoring instance") - _, err = wait.RestoreInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId, model.BackupId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Restoring instance", func() error { + _, err = wait.RestoreInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.BackupId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MongoDB Flex instance restoration: %w", err) } - s.Stop() } - params.Printer.Outputf("Restored instance %q with backup %q\n", model.InstanceId, model.BackupId) + operationState := "Restored" + if model.Async { + operationState = "Triggered restore of" + } + params.Printer.Outputf("%s instance %q with backup %q\n", operationState, model.InstanceId, model.BackupId) return nil } @@ -121,16 +124,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } if !model.Async { - s := spinner.New(params.Printer) - s.Start("Cloning instance") - _, err = wait.CloneInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Cloning instance", func() error { + _, err = wait.CloneInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MongoDB Flex instance cloning: %w", err) } - s.Stop() } - params.Printer.Outputf("Cloned instance %q from backup with timestamp %q\n", model.InstanceId, model.Timestamp) + operationState := "Cloned" + if model.Async { + operationState = "Triggered clone of" + } + params.Printer.Outputf("%s instance %q from backup with timestamp %q\n", operationState, model.InstanceId, model.Timestamp) return nil }, } @@ -148,7 +155,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -171,32 +178,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Timestamp: flags.FlagToStringValue(p, cmd, timestampFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRestoreRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiRestoreInstanceRequest { - req := apiClient.RestoreInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.RestoreInstance(ctx, model.ProjectId, model.InstanceId, model.Region) req = req.RestoreInstancePayload(mongodbflex.RestoreInstancePayload{ - BackupId: &model.BackupId, - InstanceId: &model.BackupInstanceId, + BackupId: model.BackupId, + InstanceId: model.BackupInstanceId, }) return req } func buildCloneRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiCloneInstanceRequest { - req := apiClient.CloneInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.CloneInstance(ctx, model.ProjectId, model.InstanceId, model.Region) req = req.CloneInstancePayload(mongodbflex.CloneInstancePayload{ Timestamp: &model.Timestamp, - InstanceId: &model.BackupInstanceId, + InstanceId: model.BackupInstanceId, }) return req } diff --git a/internal/cmd/mongodbflex/backup/restore/restore_test.go b/internal/cmd/mongodbflex/backup/restore/restore_test.go index 27b1d3181..e1bc0639d 100644 --- a/internal/cmd/mongodbflex/backup/restore/restore_test.go +++ b/internal/cmd/mongodbflex/backup/restore/restore_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) type testCtxKey struct{} @@ -24,7 +23,7 @@ const ( ) var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -62,10 +61,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRestoreRequest(mods ...func(request mongodbflex.ApiRestoreInstanceRequest)) mongodbflex.ApiRestoreInstanceRequest { - request := testClient.RestoreInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.RestoreInstance(testCtx, testProjectId, testInstanceId, testRegion) request = request.RestoreInstancePayload(mongodbflex.RestoreInstancePayload{ - BackupId: utils.Ptr(testBackupId), - InstanceId: utils.Ptr(testBackupInstanceId), + BackupId: testBackupId, + InstanceId: testBackupInstanceId, }) for _, mod := range mods { mod(request) @@ -74,10 +73,10 @@ func fixtureRestoreRequest(mods ...func(request mongodbflex.ApiRestoreInstanceRe } func fixtureCloneRequest(mods ...func(request mongodbflex.ApiCloneInstanceRequest)) mongodbflex.ApiCloneInstanceRequest { - request := testClient.CloneInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.CloneInstance(testCtx, testProjectId, testInstanceId, testRegion) request = request.CloneInstancePayload(mongodbflex.CloneInstancePayload{ Timestamp: utils.Ptr(testTimestamp), - InstanceId: utils.Ptr(testBackupInstanceId), + InstanceId: testBackupInstanceId, }) for _, mod := range mods { mod(request) @@ -88,6 +87,7 @@ func fixtureCloneRequest(mods ...func(request mongodbflex.ApiCloneInstanceReques func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -171,54 +171,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flag groups: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -242,7 +195,7 @@ func TestBuildRestoreRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx)) + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{})) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -272,7 +225,7 @@ func TestBuildCloneRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx)) + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{})) if diff != "" { t.Fatalf("Data does not match: %s", diff) } diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule.go b/internal/cmd/mongodbflex/backup/schedule/schedule.go index 9117e83d9..aff91fc43 100644 --- a/internal/cmd/mongodbflex/backup/schedule/schedule.go +++ b/internal/cmd/mongodbflex/backup/schedule/schedule.go @@ -2,12 +2,13 @@ package schedule import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( @@ -29,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "schedule", Short: "Shows details of the backup schedule and retention policy of a MongoDB Flex instance", @@ -43,9 +43,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format`, "$ stackit mongodbflex backup schedule --instance-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -76,7 +76,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -87,20 +87,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { InstanceId: *flags.FlagToStringPointer(p, cmd, instanceIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region) return req } @@ -127,24 +119,7 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I output.WeeklySnapshotRetentionWeeks = (*instance.Options)["weeklySnapshotRetentionWeeks"] } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(output, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, output, func() error { table := tables.NewTable() table.AddRow("BACKUP SCHEDULE (UTC)", output.BackupSchedule) table.AddSeparator() @@ -165,5 +140,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule_test.go b/internal/cmd/mongodbflex/backup/schedule/schedule_test.go index 1f67e3e64..8b274ed8b 100644 --- a/internal/cmd/mongodbflex/backup/schedule/schedule_test.go +++ b/internal/cmd/mongodbflex/backup/schedule/schedule_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -22,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -54,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -64,6 +63,7 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -125,48 +125,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -190,7 +149,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -222,11 +181,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go index af23cdef6..b089c8d48 100644 --- a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go @@ -5,8 +5,11 @@ import ( "fmt" "strconv" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" mongoDBflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( @@ -29,11 +31,11 @@ const ( // Default values for the backup schedule options defaultBackupSchedule = "0 0/6 * * *" - defaultSnapshotRetentionDays int64 = 3 - defaultDailySnapshotRetentionDays int64 = 0 - defaultWeeklySnapshotRetentionWeeks int64 = 3 - defaultMonthlySnapshotRetentionMonths int64 = 1 - defaultPointInTimeWindowHours int64 = 30 + defaultSnapshotRetentionDays int32 = 3 + defaultDailySnapshotRetentionDays int32 = 0 + defaultWeeklySnapshotRetentionWeeks int32 = 3 + defaultMonthlySnapshotRetentionMonths int32 = 1 + defaultPointInTimeWindowHours int32 = 30 ) type inputModel struct { @@ -41,13 +43,13 @@ type inputModel struct { InstanceId *string BackupSchedule *string - SnapshotRetentionDays *int64 - DailySnaphotRetentionDays *int64 - WeeklySnapshotRetentionWeeks *int64 - MonthlySnapshotRetentionMonths *int64 + SnapshotRetentionDays *int32 + DailySnaphotRetentionDays *int32 + WeeklySnapshotRetentionWeeks *int32 + MonthlySnapshotRetentionMonths *int32 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "update-schedule", Short: "Updates the backup schedule and retention policy for a MongoDB Flex instance", @@ -67,10 +69,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit mongodbflex backup update-schedule --instance-id xxx --store-for-days 5"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -81,18 +83,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongoDBflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId, model.Region) + instanceLabel, err := mongoDBflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, *model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = *model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Get current instance @@ -131,17 +131,17 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } schedule := flags.FlagToStringPointer(p, cmd, scheduleFlag) - snapshotRetentionDays := flags.FlagToInt64Pointer(p, cmd, snapshotRetentionDaysFlag) - dailySnapshotRetentionDays := flags.FlagToInt64Pointer(p, cmd, dailySnapshotRetentionDaysFlag) - weeklySnapshotRetentionWeeks := flags.FlagToInt64Pointer(p, cmd, weeklySnapshotRetentionWeeksFlag) - monthlySnapshotRetentionMonths := flags.FlagToInt64Pointer(p, cmd, monthlySnapshotRetentionMonthsFlag) + snapshotRetentionDays := flags.FlagToInt32Pointer(p, cmd, snapshotRetentionDaysFlag) + dailySnapshotRetentionDays := flags.FlagToInt32Pointer(p, cmd, dailySnapshotRetentionDaysFlag) + weeklySnapshotRetentionWeeks := flags.FlagToInt32Pointer(p, cmd, weeklySnapshotRetentionWeeksFlag) + monthlySnapshotRetentionMonths := flags.FlagToInt32Pointer(p, cmd, monthlySnapshotRetentionMonthsFlag) if schedule == nil && snapshotRetentionDays == nil && dailySnapshotRetentionDays == nil && weeklySnapshotRetentionWeeks == nil && monthlySnapshotRetentionMonths == nil { return nil, &cliErr.EmptyUpdateError{} @@ -159,7 +159,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } func buildUpdateBackupScheduleRequest(ctx context.Context, model *inputModel, instance *mongodbflex.Instance, apiClient *mongodbflex.APIClient) mongodbflex.ApiUpdateBackupScheduleRequest { - req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, *model.InstanceId, model.Region) + req := apiClient.DefaultAPI.UpdateBackupSchedule(ctx, model.ProjectId, *model.InstanceId, model.Region) payload := getUpdateBackupSchedulePayload(instance) @@ -196,25 +196,40 @@ func getUpdateBackupSchedulePayload(instance *mongodbflex.Instance) mongodbflex. if backupSchedule == nil { backupSchedule = utils.Ptr(defaultBackupSchedule) } - dailySnapshotRetentionDays, err := strconv.ParseInt(options["dailySnapshotRetentionDays"], 10, 64) + parsedDailySnapshotRetentionDays, err := strconv.ParseInt(options["dailySnapshotRetentionDays"], 10, 32) + var dailySnapshotRetentionDays int32 if err != nil { dailySnapshotRetentionDays = defaultDailySnapshotRetentionDays + } else { + dailySnapshotRetentionDays = int32(parsedDailySnapshotRetentionDays) } - weeklySnapshotRetentionWeeks, err := strconv.ParseInt(options["weeklySnapshotRetentionWeeks"], 10, 64) + parsedWeeklySnapshotRetentionWeeks, err := strconv.ParseInt(options["weeklySnapshotRetentionWeeks"], 10, 32) + var weeklySnapshotRetentionWeeks int32 if err != nil { weeklySnapshotRetentionWeeks = defaultWeeklySnapshotRetentionWeeks + } else { + weeklySnapshotRetentionWeeks = int32(parsedWeeklySnapshotRetentionWeeks) } - monthlySnapshotRetentionMonths, err := strconv.ParseInt(options["monthlySnapshotRetentionMonths"], 10, 64) + parsedMonthlySnapshotRetentionMonths, err := strconv.ParseInt(options["monthlySnapshotRetentionMonths"], 10, 32) + var monthlySnapshotRetentionMonths int32 if err != nil { monthlySnapshotRetentionMonths = defaultMonthlySnapshotRetentionMonths + } else { + monthlySnapshotRetentionMonths = int32(parsedMonthlySnapshotRetentionMonths) } - pointInTimeWindowHours, err := strconv.ParseInt(options["pointInTimeWindowHours"], 10, 64) + parsedPointInTimeWindowHours, err := strconv.ParseInt(options["pointInTimeWindowHours"], 10, 32) + var pointInTimeWindowHours int32 if err != nil { pointInTimeWindowHours = defaultPointInTimeWindowHours + } else { + pointInTimeWindowHours = int32(parsedPointInTimeWindowHours) } - snapshotRetentionDays, err := strconv.ParseInt(options["snapshotRetentionDays"], 10, 64) + parsedSnapshotRetentionDays, err := strconv.ParseInt(options["snapshotRetentionDays"], 10, 32) + var snapshotRetentionDays int32 if err != nil { snapshotRetentionDays = defaultSnapshotRetentionDays + } else { + snapshotRetentionDays = int32(parsedSnapshotRetentionDays) } defaultPayload := mongodbflex.UpdateBackupSchedulePayload{ @@ -229,6 +244,6 @@ func getUpdateBackupSchedulePayload(instance *mongodbflex.Instance) mongodbflex. } func buildGetInstanceRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, *model.InstanceId, model.Region) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, *model.InstanceId, model.Region) return req } diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go index 77b6c5c2c..f4aa91e2b 100644 --- a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go @@ -5,12 +5,13 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -21,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -57,11 +58,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixturePayload(mods ...func(payload *mongodbflex.UpdateBackupSchedulePayload)) mongodbflex.UpdateBackupSchedulePayload { payload := mongodbflex.UpdateBackupSchedulePayload{ BackupSchedule: utils.Ptr(testSchedule), - SnapshotRetentionDays: utils.Ptr(int64(3)), - DailySnapshotRetentionDays: utils.Ptr(int64(0)), - WeeklySnapshotRetentionWeeks: utils.Ptr(int64(3)), - MonthlySnapshotRetentionMonths: utils.Ptr(int64(1)), - PointInTimeWindowHours: utils.Ptr(int64(30)), + SnapshotRetentionDays: utils.Ptr(int32(3)), + DailySnapshotRetentionDays: utils.Ptr(int32(0)), + WeeklySnapshotRetentionWeeks: utils.Ptr(int32(3)), + MonthlySnapshotRetentionMonths: utils.Ptr(int32(1)), + PointInTimeWindowHours: utils.Ptr(int32(30)), } for _, mod := range mods { mod(&payload) @@ -70,7 +71,7 @@ func fixturePayload(mods ...func(payload *mongodbflex.UpdateBackupSchedulePayloa } func fixtureUpdateBackupScheduleRequest(mods ...func(request *mongodbflex.ApiUpdateBackupScheduleRequest)) mongodbflex.ApiUpdateBackupScheduleRequest { - request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId, testRegion) request = request.UpdateBackupSchedulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -79,7 +80,7 @@ func fixtureUpdateBackupScheduleRequest(mods ...func(request *mongodbflex.ApiUpd } func fixtureGetInstanceRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -106,6 +107,7 @@ func fixtureInstance(mods ...func(instance *mongodbflex.Instance)) *mongodbflex. func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -175,45 +177,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -237,7 +201,7 @@ func TestBuildGetInstanceRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -267,12 +231,12 @@ func TestBuildUpdateBackupScheduleRequest(t *testing.T) { Region: testRegion, }, InstanceId: utils.Ptr(testInstanceId), - DailySnaphotRetentionDays: utils.Ptr(int64(2)), + DailySnaphotRetentionDays: utils.Ptr(int32(2)), }, instance: fixtureInstance(), expectedRequest: fixtureUpdateBackupScheduleRequest().UpdateBackupSchedulePayload( fixturePayload(func(payload *mongodbflex.UpdateBackupSchedulePayload) { - payload.DailySnapshotRetentionDays = utils.Ptr(int64(2)) + payload.DailySnapshotRetentionDays = utils.Ptr(int32(2)) }), ), }, @@ -285,19 +249,19 @@ func TestBuildUpdateBackupScheduleRequest(t *testing.T) { }, InstanceId: utils.Ptr(testInstanceId), BackupSchedule: utils.Ptr("0 0/6 5 2 1"), - DailySnaphotRetentionDays: utils.Ptr(int64(2)), - WeeklySnapshotRetentionWeeks: utils.Ptr(int64(2)), - MonthlySnapshotRetentionMonths: utils.Ptr(int64(2)), - SnapshotRetentionDays: utils.Ptr(int64(2)), + DailySnaphotRetentionDays: utils.Ptr(int32(2)), + WeeklySnapshotRetentionWeeks: utils.Ptr(int32(2)), + MonthlySnapshotRetentionMonths: utils.Ptr(int32(2)), + SnapshotRetentionDays: utils.Ptr(int32(2)), }, instance: fixtureInstance(), expectedRequest: fixtureUpdateBackupScheduleRequest().UpdateBackupSchedulePayload( fixturePayload(func(payload *mongodbflex.UpdateBackupSchedulePayload) { payload.BackupSchedule = utils.Ptr("0 0/6 5 2 1") - payload.DailySnapshotRetentionDays = utils.Ptr(int64(2)) - payload.WeeklySnapshotRetentionWeeks = utils.Ptr(int64(2)) - payload.MonthlySnapshotRetentionMonths = utils.Ptr(int64(2)) - payload.SnapshotRetentionDays = utils.Ptr(int64(2)) + payload.DailySnapshotRetentionDays = utils.Ptr(int32(2)) + payload.WeeklySnapshotRetentionWeeks = utils.Ptr(int32(2)) + payload.MonthlySnapshotRetentionMonths = utils.Ptr(int32(2)) + payload.SnapshotRetentionDays = utils.Ptr(int32(2)) }), ), }, @@ -321,7 +285,7 @@ func TestBuildUpdateBackupScheduleRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mongodbflex/instance/create/create.go b/internal/cmd/mongodbflex/instance/create/create.go index 28a65e394..9dcf3b23e 100644 --- a/internal/cmd/mongodbflex/instance/create/create.go +++ b/internal/cmd/mongodbflex/instance/create/create.go @@ -2,13 +2,15 @@ package create import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,8 +22,6 @@ import ( mongodbflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" ) const ( @@ -34,7 +34,6 @@ const ( storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" - typeFlag = "type" defaultBackupSchedule = "0 0/6 * * *" defaultStorageClass = "premium-perf2-mongodb" @@ -42,22 +41,29 @@ const ( defaultType = "Replica" ) +var typeFlag = flags.StringEnumFlag( + "type", + mongodbflexUtils.AvailableInstanceTypes(), + "Instance type,", + flags.StringEnumDefaultValue(defaultType), +) + type inputModel struct { *globalflags.GlobalFlagModel - InstanceName *string - ACL *[]string - BackupSchedule *string - FlavorId *string - CPU *int64 - RAM *int64 + InstanceName string + ACL []string + BackupSchedule string + FlavorId string + CPU *int32 + RAM *int32 StorageClass *string StorageSize *int64 - Version *string + Version string Type *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a MongoDB Flex instance", @@ -74,10 +80,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a MongoDB Flex instance with name "my-instance", allow access to a specific range of IP addresses, specify flavor by CPU and RAM and set storage size to 20 GB. Other parameters are set to default values`, `$ stackit mongodbflex instance create --name my-instance --cpu 1 --ram 4 --acl 1.2.3.0/24 --storage-size 20`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -94,25 +100,23 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a MongoDB Flex instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a MongoDB Flex instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Fill in version, if needed - if model.Version == nil { - version, err := mongodbflexUtils.GetLatestMongoDBVersion(ctx, apiClient, model.ProjectId, model.Region) + if model.Version == "" { + version, err := mongodbflexUtils.GetLatestMongoDBVersion(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) if err != nil { return fmt.Errorf("get latest MongoDB version: %w", err) } - model.Version = &version + model.Version = version } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -124,13 +128,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MongoDB Flex instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) @@ -141,24 +145,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - typeFlagOptions := mongodbflexUtils.AvailableInstanceTypes() - cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc.") cmd.Flags().String(backupScheduleFlag, defaultBackupSchedule, "Backup schedule") cmd.Flags().String(flavorIdFlag, "", "ID of the flavor") - cmd.Flags().Int64(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB)") + cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") + cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") cmd.Flags().String(storageClassFlag, defaultStorageClass, "Storage class") cmd.Flags().Int64(storageSizeFlag, defaultStorageSize, "Storage size (in GB)") cmd.Flags().String(versionFlag, "", "MongoDB version. Defaults to the latest version available") - cmd.Flags().Var(flags.EnumFlag(false, defaultType, typeFlagOptions...), typeFlag, fmt.Sprintf("Instance type, one of %q", typeFlagOptions)) + typeFlag.Register(cmd.Flags()) err := flags.MarkFlagsRequired(cmd, instanceNameFlag, aclFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -166,16 +168,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { storageSize := flags.FlagWithDefaultToInt64Value(p, cmd, storageSizeFlag) - flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) - cpu := flags.FlagToInt64Pointer(p, cmd, cpuFlag) - ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) + flavorId := flags.FlagToStringValue(p, cmd, flavorIdFlag) + cpu := flags.FlagToInt32Pointer(p, cmd, cpuFlag) + ram := flags.FlagToInt32Pointer(p, cmd, ramFlag) - if flavorId == nil && (cpu == nil || ram == nil) { + if flavorId == "" && (cpu == nil || ram == nil) { return nil, &cliErr.DatabaseInputFlavorError{ Cmd: cmd, } } - if flavorId != nil && (cpu != nil || ram != nil) { + if flavorId != "" && (cpu != nil || ram != nil) { return nil, &cliErr.DatabaseInputFlavorError{ Cmd: cmd, } @@ -183,49 +185,41 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), - ACL: flags.FlagToStringSlicePointer(p, cmd, aclFlag), - BackupSchedule: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, backupScheduleFlag)), + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), + ACL: flags.FlagToStringSliceValue(p, cmd, aclFlag), + BackupSchedule: flags.FlagWithDefaultToStringValue(p, cmd, backupScheduleFlag), FlavorId: flavorId, CPU: cpu, RAM: ram, StorageClass: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, storageClassFlag)), StorageSize: &storageSize, - Version: flags.FlagToStringPointer(p, cmd, versionFlag), - Type: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, typeFlag)), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Version: flags.FlagToStringValue(p, cmd, versionFlag), + Type: typeFlag.Ptr(), } + p.DebugInputModel(model) return &model, nil } type MongoDBFlexClient interface { CreateInstance(ctx context.Context, projectId, region string) mongodbflex.ApiCreateInstanceRequest - ListFlavorsExecute(ctx context.Context, projectId, region string) (*mongodbflex.ListFlavorsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, flavorId, region string) (*mongodbflex.ListStoragesResponse, error) + ListFlavors(ctx context.Context, projectId, region string) mongodbflex.ApiListFlavorsRequest + ListStorages(ctx context.Context, projectId, flavorId, region string) mongodbflex.ApiListStoragesRequest } func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexClient) (mongodbflex.ApiCreateInstanceRequest, error) { req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) - var flavorId *string + var flavorId string var err error - flavors, err := apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex flavors: %w", err) } - if model.FlavorId == nil { - flavorId, err = mongodbflexUtils.LoadFlavorId(*model.CPU, *model.RAM, flavors.Flavors) + if model.FlavorId == "" { + foundFlavorId, err := mongodbflexUtils.LoadFlavorId(*model.CPU, *model.RAM, &flavors.Flavors) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -233,19 +227,20 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC } return req, err } + flavorId = *foundFlavorId } else { - err := mongodbflexUtils.ValidateFlavorId(*model.FlavorId, flavors.Flavors) + err := mongodbflexUtils.ValidateFlavorId(model.FlavorId, flavors.Flavors) if err != nil { return req, err } flavorId = model.FlavorId } - storages, err := apiClient.ListStoragesExecute(ctx, model.ProjectId, *flavorId, model.Region) + storages, err := apiClient.ListStorages(ctx, model.ProjectId, flavorId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex storages: %w", err) } - err = mongodbflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *flavorId) + err = mongodbflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, flavorId) if err != nil { return req, err } @@ -257,18 +252,18 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC req = req.CreateInstancePayload(mongodbflex.CreateInstancePayload{ Name: model.InstanceName, - Acl: &mongodbflex.CreateInstancePayloadAcl{Items: model.ACL}, + Acl: mongodbflex.ACL{Items: model.ACL}, BackupSchedule: model.BackupSchedule, FlavorId: flavorId, - Replicas: &replicas, - Storage: &mongodbflex.Storage{ + Replicas: replicas, + Storage: mongodbflex.Storage{ Class: model.StorageClass, Size: model.StorageSize, }, Version: model.Version, - Options: utils.Ptr(map[string]string{ + Options: map[string]string{ "type": *model.Type, - }), + }, }) return req, nil } @@ -278,29 +273,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe return fmt.Errorf("create instance response is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDBFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDBFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, utils.PtrString(resp.Id)) return nil - } + }) } diff --git a/internal/cmd/mongodbflex/instance/create/create_test.go b/internal/cmd/mongodbflex/instance/create/create_test.go index 8d2bd7aba..4f1b88082 100644 --- a/internal/cmd/mongodbflex/instance/create/create_test.go +++ b/internal/cmd/mongodbflex/instance/create/create_test.go @@ -5,15 +5,15 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -23,36 +23,34 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} -type mongoDBFlexClientMocked struct { - listFlavorsFails bool - listFlavorsResp *mongodbflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *mongodbflex.ListStoragesResponse +type mockSettings struct { + listFlavorsFails bool + listFlavorsResp *mongodbflex.ListFlavorsResponse + listStoragesResp *mongodbflex.ListStoragesResponse } -func (c *mongoDBFlexClientMocked) CreateInstance(ctx context.Context, projectId, region string) mongodbflex.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId, region) -} - -func (c *mongoDBFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*mongodbflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil -} +var testProjectId = uuid.NewString() +var testFlavorId = uuid.NewString() -func (c *mongoDBFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*mongodbflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") +func newAPICLientMock(settings mockSettings) mongodbflex.DefaultAPI { + return mongodbflex.DefaultAPIServiceMock{ + ListStoragesExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListStoragesRequest) (*mongodbflex.ListStoragesResponse, error) { + if settings.listFlavorsFails { + return nil, fmt.Errorf("list storages failed") + } + return settings.listStoragesResp, nil + }), + ListFlavorsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListFlavorsRequest) (*mongodbflex.ListFlavorsResponse, error) { + if settings.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return settings.listFlavorsResp, nil + }), } - return c.listFlavorsResp, nil } -var testProjectId = uuid.NewString() -var testFlavorId = uuid.NewString() - func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, @@ -64,7 +62,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st storageClassFlag: "premium-perf4-mongodb", // Non-default storageSizeFlag: "10", versionFlag: "6.0", - typeFlag: "Replica", + typeFlag.Name(): "Replica", } for _, mod := range mods { mod(flagValues) @@ -79,13 +77,13 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), - ACL: utils.Ptr([]string{"0.0.0.0/0"}), - BackupSchedule: utils.Ptr("0 0/6 * * *"), - FlavorId: utils.Ptr(testFlavorId), + InstanceName: "example-name", + ACL: []string{"0.0.0.0/0"}, + BackupSchedule: "0 0/6 * * *", + FlavorId: testFlavorId, StorageClass: utils.Ptr("premium-perf4-mongodb"), StorageSize: utils.Ptr(int64(10)), - Version: utils.Ptr("6.0"), + Version: "6.0", Type: utils.Ptr("Replica"), } for _, mod := range mods { @@ -95,7 +93,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiCreateInstanceRequest)) mongodbflex.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion) request = request.CreateInstancePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -105,19 +103,19 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiCreateInstanceRequest)) func fixturePayload(mods ...func(payload *mongodbflex.CreateInstancePayload)) mongodbflex.CreateInstancePayload { payload := mongodbflex.CreateInstancePayload{ - Name: utils.Ptr("example-name"), - Acl: &mongodbflex.CreateInstancePayloadAcl{Items: utils.Ptr([]string{"0.0.0.0/0"})}, - BackupSchedule: utils.Ptr("0 0/6 * * *"), - FlavorId: utils.Ptr(testFlavorId), - Replicas: utils.Ptr(int64(3)), - Storage: &mongodbflex.Storage{ + Name: "example-name", + Acl: mongodbflex.ACL{Items: []string{"0.0.0.0/0"}}, + BackupSchedule: "0 0/6 * * *", + FlavorId: testFlavorId, + Replicas: int32(3), + Storage: mongodbflex.Storage{ Class: utils.Ptr("premium-perf4-mongodb"), Size: utils.Ptr(int64(10)), }, - Version: utils.Ptr("6.0"), - Options: utils.Ptr(map[string]string{ + Version: "6.0", + Options: map[string]string{ "type": "Replica", - }), + }, } for _, mod := range mods { mod(&payload) @@ -128,6 +126,7 @@ func fixturePayload(mods ...func(payload *mongodbflex.CreateInstancePayload)) mo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -143,7 +142,7 @@ func TestParseInput(t *testing.T) { description: "with defaults", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, backupScheduleFlag) - delete(flagValues, typeFlag) + delete(flagValues, typeFlag.Name()) }), isValid: true, expectedModel: fixtureInputModel(), @@ -157,9 +156,9 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.FlavorId = "" + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }), }, { @@ -217,7 +216,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Version = nil + model.Version = "" }), }, { @@ -226,9 +225,8 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.ACL = utils.Ptr( - append(*model.ACL, "198.51.100.14/24", "198.51.100.14/32"), - ) + model.ACL = + append(model.ACL, "198.51.100.14/24", "198.51.100.14/32") }), }, { @@ -237,9 +235,8 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.ACL = utils.Ptr( - append(*model.ACL, "198.51.100.14/24", "198.51.100.14/32"), - ) + model.ACL = + append(model.ACL, "198.51.100.14/24", "198.51.100.14/32") }), }, { @@ -254,90 +251,42 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.aclValues { - err := cmd.Flags().Set(aclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", aclFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + aclFlag: tt.aclValues, + }, tt.isValid) }) } } func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest mongodbflex.ApiCreateInstanceRequest - listFlavorsFails bool - listFlavorsResp *mongodbflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *mongodbflex.ListStoragesResponse - isValid bool + description string + model *inputModel + expectedRequest mongodbflex.ApiCreateInstanceRequest + mockClientSettings mockSettings + isValid bool }{ { description: "base with flavor ID", model: fixtureInputModel(), isValid: true, expectedRequest: fixtureRequest(), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-mongodb"}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, @@ -345,32 +294,34 @@ func TestBuildRequest(t *testing.T) { description: "with CPU and RAM", model: fixtureInputModel( func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.FlavorId = "" + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), isValid: true, expectedRequest: fixtureRequest(), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, + { + Id: utils.Ptr("other-flavor"), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), + }, }, }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-mongodb"}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, @@ -379,23 +330,25 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(func(model *inputModel) { model.Type = utils.Ptr("Single") }), isValid: true, expectedRequest: fixtureRequest().CreateInstancePayload(fixturePayload(func(payload *mongodbflex.CreateInstancePayload) { - payload.Options = utils.Ptr(map[string]string{"type": "Single"}) - payload.Replicas = utils.Ptr(int64(1)) + payload.Options = map[string]string{"type": "Single"} + payload.Replicas = int32(1) })), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-mongodb"}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, @@ -404,23 +357,25 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(func(model *inputModel) { model.Type = utils.Ptr("Sharded") }), isValid: true, expectedRequest: fixtureRequest().CreateInstancePayload(fixturePayload(func(payload *mongodbflex.CreateInstancePayload) { - payload.Options = utils.Ptr(map[string]string{"type": "Sharded"}) - payload.Replicas = utils.Ptr(int64(9)) + payload.Options = map[string]string{"type": "Sharded"} + payload.Replicas = int32(9) })), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-mongodb"}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, @@ -428,34 +383,38 @@ func TestBuildRequest(t *testing.T) { description: "get flavors fails", model: fixtureInputModel( func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.FlavorId = "" + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "flavor id not found", model: fixtureInputModel( func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(5)) - model.RAM = utils.Ptr(int64(9)) + model.FlavorId = "" + model.CPU = utils.Ptr(int32(5)) + model.RAM = utils.Ptr(int32(9)) }, ), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, + { + Id: utils.Ptr("other-flavor"), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), + }, }, }, }, @@ -465,13 +424,15 @@ func TestBuildRequest(t *testing.T) { description: "get storages fails", model: fixtureInputModel( func(model *inputModel) { - model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.FlavorId = "" + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "invalid storage class", @@ -480,20 +441,22 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("non-existing-class") }, ), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-mongodb"}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -505,20 +468,22 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(9)) }, ), - listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), + }, }, }, - }, - listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-mongodb"}, - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &mongodbflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-mongodb"}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -527,13 +492,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mongoDBFlexClientMocked{ - listFlavorsFails: tt.listFlavorsFails, - listFlavorsResp: tt.listFlavorsResp, - listStoragesFails: tt.listStoragesFails, - listStoragesResp: tt.listStoragesResp, - } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPICLientMock(tt.mockClientSettings)) if err != nil { if !tt.isValid { return @@ -544,6 +503,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -577,11 +537,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.createInstanceResponse); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.createInstanceResponse); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/instance/delete/delete.go b/internal/cmd/mongodbflex/instance/delete/delete.go index b63d61d63..a9ca077bd 100644 --- a/internal/cmd/mongodbflex/instance/delete/delete.go +++ b/internal/cmd/mongodbflex/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a MongoDB Flex instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MongoDB Flex instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.InstanceId, model.Region) return req } diff --git a/internal/cmd/mongodbflex/instance/delete/delete_test.go b/internal/cmd/mongodbflex/instance/delete/delete_test.go index 8add607ed..66753f699 100644 --- a/internal/cmd/mongodbflex/instance/delete/delete_test.go +++ b/internal/cmd/mongodbflex/instance/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -21,7 +20,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiDeleteInstanceRequest)) mongodbflex.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -142,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -213,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mongodbflex/instance/describe/describe.go b/internal/cmd/mongodbflex/instance/describe/describe.go index 0a5c11e54..1e6dd2652 100644 --- a/internal/cmd/mongodbflex/instance/describe/describe.go +++ b/internal/cmd/mongodbflex/instance/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a MongoDB Flex instance", @@ -83,46 +82,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.Instance) error { - if instance == nil { - return fmt.Errorf("instance is nil") - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex instance: %w", err) + return p.OutputResult(outputFormat, instance, func() error { + if instance == nil { + return fmt.Errorf("instance is nil") } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: var instanceType string if instance.HasReplicas() { var err error @@ -147,8 +120,7 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I table.AddRow("VERSION", utils.PtrString(instance.Version)) table.AddSeparator() if instance.HasAcl() { - aclsArray := *instance.Acl.Items - acls := strings.Join(aclsArray, ",") + acls := strings.Join(instance.Acl.Items, ",") table.AddRow("ACL", acls) table.AddSeparator() } @@ -176,5 +148,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/instance/describe/describe_test.go b/internal/cmd/mongodbflex/instance/describe/describe_test.go index aa0235465..0a6e0cdf3 100644 --- a/internal/cmd/mongodbflex/instance/describe/describe_test.go +++ b/internal/cmd/mongodbflex/instance/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -21,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -62,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -142,54 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -213,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -245,11 +198,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/instance/instance.go b/internal/cmd/mongodbflex/instance/instance.go index a4cc5b231..ee48a0632 100644 --- a/internal/cmd/mongodbflex/instance/instance.go +++ b/internal/cmd/mongodbflex/instance/instance.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/instance/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for MongoDB Flex instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/mongodbflex/instance/list/list.go b/internal/cmd/mongodbflex/instance/list/list.go index fa30c47ca..4e362a40c 100644 --- a/internal/cmd/mongodbflex/instance/list/list.go +++ b/internal/cmd/mongodbflex/instance/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all MongoDB Flex instances", @@ -48,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 MongoDB Flex instances`, "$ stackit mongodbflex instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,23 +66,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get MongoDB Flex instances: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + instances := resp.Items + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - instances := *resp.Items // Truncate output if model.Limit != nil && len(instances) > int(*model.Limit) { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -95,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -114,42 +110,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId, model.Region).Tag("") + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId, model.Region).Tag("") return req } -func outputResult(p *print.Printer, outputFormat string, instances []mongodbflex.InstanceListInstance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []mongodbflex.InstanceListInstance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATUS") for i := range instances { @@ -166,5 +142,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []mongodbflex } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/instance/list/list_test.go b/internal/cmd/mongodbflex/instance/list/list_test.go index 9c558f205..988be4ae4 100644 --- a/internal/cmd/mongodbflex/instance/list/list_test.go +++ b/internal/cmd/mongodbflex/instance/list/list_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -23,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -54,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiListInstancesRequest)) mongodbflex.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId, testRegion).Tag("") + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId, testRegion).Tag("") for _, mod := range mods { mod(&request) } @@ -64,6 +63,7 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiListInstancesRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -118,48 +118,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -183,7 +142,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -195,6 +154,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instanceList []mongodbflex.InstanceListInstance } tests := []struct { @@ -222,11 +182,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instanceList); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instanceList); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/instance/update/update.go b/internal/cmd/mongodbflex/instance/update/update.go index 5c11a7ee6..22ba42eda 100644 --- a/internal/cmd/mongodbflex/instance/update/update.go +++ b/internal/cmd/mongodbflex/instance/update/update.go @@ -2,12 +2,11 @@ package update import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,8 +19,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api/wait" ) const ( @@ -36,7 +35,12 @@ const ( storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" - typeFlag = "type" +) + +var typeFlag = flags.StringEnumFlag( + "type", + mongodbflexUtils.AvailableInstanceTypes(), + "Instance type,", ) type inputModel struct { @@ -47,15 +51,15 @@ type inputModel struct { ACL *[]string BackupSchedule *string FlavorId *string - CPU *int64 - RAM *int64 + CPU *int32 + RAM *int32 StorageClass *string StorageSize *int64 Version *string Type *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a MongoDB Flex instance", @@ -83,22 +87,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -110,13 +112,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, instanceId, model.Region).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for MongoDB Flex instance update: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, resp) @@ -127,18 +129,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - typeFlagOptions := mongodbflexUtils.AvailableInstanceTypes() - cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "Lists of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().String(backupScheduleFlag, "", "Backup schedule") cmd.Flags().String(flavorIdFlag, "", "ID of the flavor") - cmd.Flags().Int64(cpuFlag, 0, "Number of CPUs") - cmd.Flags().Int64(ramFlag, 0, "Amount of RAM (in GB)") + cmd.Flags().Int32(cpuFlag, 0, "Number of CPUs") + cmd.Flags().Int32(ramFlag, 0, "Amount of RAM (in GB)") cmd.Flags().String(storageClassFlag, "", "Storage class") cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB)") cmd.Flags().String(versionFlag, "", "Version") - cmd.Flags().Var(flags.EnumFlag(false, "", typeFlagOptions...), typeFlag, fmt.Sprintf("Instance type, one of %q", typeFlagOptions)) + typeFlag.Register(cmd.Flags()) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -151,14 +151,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu instanceName := flags.FlagToStringPointer(p, cmd, instanceNameFlag) flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) - cpu := flags.FlagToInt64Pointer(p, cmd, cpuFlag) - ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) + cpu := flags.FlagToInt32Pointer(p, cmd, cpuFlag) + ram := flags.FlagToInt32Pointer(p, cmd, ramFlag) acl := flags.FlagToStringSlicePointer(p, cmd, aclFlag) backupSchedule := flags.FlagToStringPointer(p, cmd, backupScheduleFlag) storageClass := flags.FlagToStringPointer(p, cmd, storageClassFlag) storageSize := flags.FlagToInt64Pointer(p, cmd, storageSizeFlag) version := flags.FlagToStringPointer(p, cmd, versionFlag) - instanceType := flags.FlagToStringPointer(p, cmd, typeFlag) + instanceType := typeFlag.Ptr() if instanceName == nil && flavorId == nil && cpu == nil && ram == nil && acl == nil && backupSchedule == nil && storageClass == nil && storageSize == nil && version == nil && instanceType == nil { @@ -187,23 +187,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Type: instanceType, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } type MongoDBFlexClient interface { PartialUpdateInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiPartialUpdateInstanceRequest - GetInstanceExecute(ctx context.Context, projectId, instanceId, region string) (*mongodbflex.InstanceResponse, error) - ListFlavorsExecute(ctx context.Context, projectId, region string) (*mongodbflex.ListFlavorsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, flavorId, region string) (*mongodbflex.ListStoragesResponse, error) + GetInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiGetInstanceRequest + ListFlavors(ctx context.Context, projectId, region string) mongodbflex.ApiListFlavorsRequest + ListStorages(ctx context.Context, projectId, flavorId, region string) mongodbflex.ApiListStoragesRequest } func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexClient) (mongodbflex.ApiPartialUpdateInstanceRequest, error) { @@ -212,7 +204,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC var flavorId *string var err error - flavors, err := apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex flavors: %w", err) } @@ -221,7 +213,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC ram := model.RAM cpu := model.CPU if model.RAM == nil || model.CPU == nil { - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.InstanceId, model.Region) + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex instance: %w", err) } @@ -232,7 +224,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC cpu = currentInstance.Item.Flavor.Cpu } } - flavorId, err = mongodbflexUtils.LoadFlavorId(*cpu, *ram, flavors.Flavors) + flavorId, err = mongodbflexUtils.LoadFlavorId(*cpu, *ram, &flavors.Flavors) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -252,13 +244,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC if model.StorageClass != nil || model.StorageSize != nil { validationFlavorId := flavorId if validationFlavorId == nil { - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.InstanceId, model.Region) + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex instance: %w", err) } validationFlavorId = currentInstance.Item.Flavor.Id } - storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, *validationFlavorId, model.Region) + storages, err = apiClient.ListStorages(ctx, model.ProjectId, *validationFlavorId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get MongoDB Flex storages: %w", err) } @@ -270,7 +262,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC var payloadAcl *mongodbflex.ACL if model.ACL != nil { - payloadAcl = &mongodbflex.ACL{Items: model.ACL} + payloadAcl = &mongodbflex.ACL{Items: *model.ACL} } var payloadStorage *mongodbflex.Storage @@ -281,7 +273,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient MongoDBFlexC } } - var replicas *int64 + var replicas *int32 var payloadOptions *map[string]string if model.Type != nil { replicasInt, err := mongodbflexUtils.GetInstanceReplicas(*model.Type) @@ -313,29 +305,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, instanceLab return fmt.Errorf("resp is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal update MongoDBFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal update MongoDBFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Updated" if async { operationState = "Triggered update of" } p.Info("%s instance %q\n", operationState, instanceLabel) return nil - } + }) } diff --git a/internal/cmd/mongodbflex/instance/update/update_test.go b/internal/cmd/mongodbflex/instance/update/update_test.go index f9bb2dffa..5b34c94f0 100644 --- a/internal/cmd/mongodbflex/instance/update/update_test.go +++ b/internal/cmd/mongodbflex/instance/update/update_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -23,9 +22,9 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} -type mongoDBFlexClientMocked struct { +type mockClientSettings struct { listFlavorsFails bool listFlavorsResp *mongodbflex.ListFlavorsResponse listStoragesFails bool @@ -34,29 +33,27 @@ type mongoDBFlexClientMocked struct { getInstanceResp *mongodbflex.InstanceResponse } -func (c *mongoDBFlexClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId, region) -} - -func (c *mongoDBFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*mongodbflex.InstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") - } - return c.getInstanceResp, nil -} - -func (c *mongoDBFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*mongodbflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil -} - -func (c *mongoDBFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*mongodbflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") +func newAPIClientMock(c mockClientSettings) mongodbflex.DefaultAPI { + return mongodbflex.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ mongodbflex.ApiGetInstanceRequest) (*mongodbflex.InstanceResponse, error) { + if c.getInstanceFails { + return nil, fmt.Errorf("get instance failed") + } + return c.getInstanceResp, nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListStoragesRequest) (*mongodbflex.ListStoragesResponse, error) { + if c.listFlavorsFails { + return nil, fmt.Errorf("list storages failed") + } + return c.listStoragesResp, nil + }), + ListFlavorsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListFlavorsRequest) (*mongodbflex.ListFlavorsResponse, error) { + if c.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return c.listFlavorsResp, nil + }), } - return c.listFlavorsResp, nil } var testProjectId = uuid.NewString() @@ -95,7 +92,7 @@ func fixtureStandardFlagValues(mods ...func(flagValues map[string]string)) map[s storageClassFlag: "class", storageSizeFlag: "10", versionFlag: "5.0", - typeFlag: "Single", + typeFlag.Name(): "Single", } for _, mod := range mods { mod(flagValues) @@ -142,7 +139,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiPartialUpdateInstanceRequest)) mongodbflex.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion) request = request.PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{}) for _, mod := range mods { mod(&request) @@ -202,8 +199,8 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureStandardInputModel(func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }), }, { @@ -286,8 +283,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + typeFlag.Reset() + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -329,7 +327,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -374,15 +372,15 @@ func TestBuildRequest(t *testing.T) { }), isValid: true, listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ + Flavors: []mongodbflex.InstanceFlavor{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{ FlavorId: utils.Ptr(testFlavorId), }), @@ -390,20 +388,20 @@ func TestBuildRequest(t *testing.T) { { description: "update flavor from cpu and ram", model: fixtureRequiredInputModel(func(model *inputModel) { - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }), isValid: true, listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ + Flavors: []mongodbflex.InstanceFlavor{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{ FlavorId: utils.Ptr(testFlavorId), }), @@ -422,13 +420,13 @@ func TestBuildRequest(t *testing.T) { }, }, listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, + StorageClasses: []string{"class"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{ Storage: &mongodbflex.Storage{ Class: utils.Ptr("class"), @@ -450,13 +448,13 @@ func TestBuildRequest(t *testing.T) { }, }, listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, + StorageClasses: []string{"class"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testInstanceId, testRegion). PartialUpdateInstancePayload(mongodbflex.PartialUpdateInstancePayload{ Storage: &mongodbflex.Storage{ Class: utils.Ptr("class"), @@ -468,8 +466,8 @@ func TestBuildRequest(t *testing.T) { description: "get flavors fails", model: fixtureRequiredInputModel( func(model *inputModel) { - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), listFlavorsFails: true, @@ -479,21 +477,21 @@ func TestBuildRequest(t *testing.T) { description: "flavor id not found", model: fixtureRequiredInputModel( func(model *inputModel) { - model.CPU = utils.Ptr(int64(5)) - model.RAM = utils.Ptr(int64(9)) + model.CPU = utils.Ptr(int32(5)) + model.RAM = utils.Ptr(int32(9)) }, ), listFlavorsResp: &mongodbflex.ListFlavorsResponse{ - Flavors: &[]mongodbflex.InstanceFlavor{ + Flavors: []mongodbflex.InstanceFlavor{ { Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + Cpu: utils.Ptr(int32(1)), + Memory: utils.Ptr(int32(8)), }, }, }, @@ -514,8 +512,8 @@ func TestBuildRequest(t *testing.T) { model: fixtureRequiredInputModel( func(model *inputModel) { model.FlavorId = nil - model.CPU = utils.Ptr(int64(2)) - model.RAM = utils.Ptr(int64(4)) + model.CPU = utils.Ptr(int32(2)) + model.RAM = utils.Ptr(int32(4)) }, ), listFlavorsFails: true, @@ -536,7 +534,7 @@ func TestBuildRequest(t *testing.T) { }, }, listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, + StorageClasses: []string{"class"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), @@ -559,7 +557,7 @@ func TestBuildRequest(t *testing.T) { }, }, listStoragesResp: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, + StorageClasses: []string{"class"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(10)), Max: utils.Ptr(int64(100)), @@ -571,7 +569,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mongoDBFlexClientMocked{ + settings := mockClientSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, listFlavorsFails: tt.listFlavorsFails, @@ -579,7 +577,7 @@ func TestBuildRequest(t *testing.T) { listStoragesFails: tt.listStoragesFails, listStoragesResp: tt.listStoragesResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIClientMock(settings)) if err != nil { if !tt.isValid { return @@ -590,6 +588,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -623,11 +622,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/mongodbflex.go b/internal/cmd/mongodbflex/mongodbflex.go index 50ba29f78..3376477a3 100644 --- a/internal/cmd/mongodbflex/mongodbflex.go +++ b/internal/cmd/mongodbflex/mongodbflex.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/options" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/user" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "mongodbflex", Short: "Provides functionality for MongoDB Flex", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(user.NewCmd(params)) cmd.AddCommand(options.NewCmd(params)) diff --git a/internal/cmd/mongodbflex/options/options.go b/internal/cmd/mongodbflex/options/options.go index 53499547e..1941ffbc1 100644 --- a/internal/cmd/mongodbflex/options/options.go +++ b/internal/cmd/mongodbflex/options/options.go @@ -2,13 +2,13 @@ package options import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -17,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( @@ -37,9 +36,9 @@ type inputModel struct { } type options struct { - Flavors *[]mongodbflex.InstanceFlavor `json:"flavors,omitempty"` - Versions *[]string `json:"versions,omitempty"` - Storages *flavorStorages `json:"flavorStorages,omitempty"` + Flavors []mongodbflex.InstanceFlavor `json:"flavors,omitempty"` + Versions []string `json:"versions,omitempty"` + Storages *flavorStorages `json:"flavorStorages,omitempty"` } type flavorStorages struct { @@ -47,7 +46,7 @@ type flavorStorages struct { Storages *mongodbflex.ListStoragesResponse `json:"storages"` } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", Short: "Lists MongoDB Flex options", @@ -64,9 +63,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List MongoDB Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit mongodbflex options --flavors"`, "$ stackit mongodbflex options --storages --flavor-id "), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -78,7 +77,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient) + err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("get MongoDB Flex options: %w", err) } @@ -97,7 +96,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(flavorIdFlag, "", `The flavor ID to show storages for. Only relevant when "--storages" is passed`) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) flavors := flags.FlagToBoolValue(p, cmd, flavorsFlag) versions := flags.FlagToBoolValue(p, cmd, versionsFlag) @@ -125,22 +124,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { FlavorId: flags.FlagToStringPointer(p, cmd, flavorIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } type mongoDBFlexOptionsClient interface { - ListFlavorsExecute(ctx context.Context, projectId, region string) (*mongodbflex.ListFlavorsResponse, error) - ListVersionsExecute(ctx context.Context, projectId, region string) (*mongodbflex.ListVersionsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, flavorId, region string) (*mongodbflex.ListStoragesResponse, error) + ListFlavors(ctx context.Context, projectId, region string) mongodbflex.ApiListFlavorsRequest + ListVersions(ctx context.Context, projectId, region string) mongodbflex.ApiListVersionsRequest + ListStorages(ctx context.Context, projectId, flavorId, region string) mongodbflex.ApiListStoragesRequest } func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputModel, apiClient mongoDBFlexOptionsClient) error { @@ -150,19 +141,19 @@ func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputM var err error if model.Flavors { - flavors, err = apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err = apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return fmt.Errorf("get MongoDB Flex flavors: %w", err) } } if model.Versions { - versions, err = apiClient.ListVersionsExecute(ctx, model.ProjectId, model.Region) + versions, err = apiClient.ListVersions(ctx, model.ProjectId, model.Region).Execute() if err != nil { return fmt.Errorf("get MongoDB Flex versions: %w", err) } } if model.Storages { - storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, *model.FlavorId, model.Region) + storages, err = apiClient.ListStorages(ctx, model.ProjectId, *model.FlavorId, model.Region).Execute() if err != nil { return fmt.Errorf("get MongoDB Flex storages: %w", err) } @@ -190,25 +181,9 @@ func outputResult(p *print.Printer, model *inputModel, flavors *mongodbflex.List } } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(options, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex options: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex options: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, options, func() error { return outputResultAsTable(p, model, options) - } + }) } func outputResultAsTable(p *print.Printer, model *inputModel, options *options) error { @@ -219,13 +194,13 @@ func outputResultAsTable(p *print.Printer, model *inputModel, options *options) } content := []tables.Table{} - if model.Flavors && len(*options.Flavors) != 0 { - content = append(content, buildFlavorsTable(*options.Flavors)) + if model.Flavors && len(options.Flavors) != 0 { + content = append(content, buildFlavorsTable(options.Flavors)) } - if model.Versions && len(*options.Versions) != 0 { - content = append(content, buildVersionsTable(*options.Versions)) + if model.Versions && len(options.Versions) != 0 { + content = append(content, buildVersionsTable(options.Versions)) } - if model.Storages && options.Storages.Storages != nil && len(*options.Storages.Storages.StorageClasses) > 0 { + if model.Storages && options.Storages.Storages != nil && len(options.Storages.Storages.StorageClasses) > 0 { content = append(content, buildStoragesTable(*options.Storages.Storages)) } @@ -248,7 +223,7 @@ func buildFlavorsTable(flavors []mongodbflex.InstanceFlavor) tables.Table { utils.PtrString(f.Cpu), utils.PtrString(f.Memory), utils.PtrString(f.Description), - utils.PtrString(f.Categories), + f.Categories, ) } return table @@ -266,16 +241,15 @@ func buildVersionsTable(versions []string) tables.Table { } func buildStoragesTable(storagesResp mongodbflex.ListStoragesResponse) tables.Table { - storages := *storagesResp.StorageClasses + storages := storagesResp.StorageClasses table := tables.NewTable() table.SetTitle("Storages") table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") - for i := range storages { - sc := storages[i] + for _, storageClass := range storages { table.AddRow( utils.PtrString(storagesResp.StorageRange.Min), utils.PtrString(storagesResp.StorageRange.Max), - sc, + storageClass, ) } table.EnableAutoMergeOnColumns(1, 2, 3) diff --git a/internal/cmd/mongodbflex/options/options_test.go b/internal/cmd/mongodbflex/options/options_test.go index 35ab363dc..667db24b9 100644 --- a/internal/cmd/mongodbflex/options/options_test.go +++ b/internal/cmd/mongodbflex/options/options_test.go @@ -5,20 +5,19 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -type mongoDBFlexClientMocked struct { +type mockSettings struct { listFlavorsFails bool listVersionsFails bool listStoragesFails bool @@ -28,38 +27,40 @@ type mongoDBFlexClientMocked struct { listStoragesCalled bool } -func (c *mongoDBFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*mongodbflex.ListFlavorsResponse, error) { - c.listFlavorsCalled = true - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") - } - return utils.Ptr(mongodbflex.ListFlavorsResponse{ - Flavors: utils.Ptr([]mongodbflex.InstanceFlavor{}), - }), nil -} - -func (c *mongoDBFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*mongodbflex.ListVersionsResponse, error) { - c.listVersionsCalled = true - if c.listVersionsFails { - return nil, fmt.Errorf("list versions failed") - } - return utils.Ptr(mongodbflex.ListVersionsResponse{ - Versions: utils.Ptr([]string{}), - }), nil -} - -func (c *mongoDBFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*mongodbflex.ListStoragesResponse, error) { - c.listStoragesCalled = true - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") +func newAPIClientMock(c *mockSettings) mongodbflex.DefaultAPI { + return mongodbflex.DefaultAPIServiceMock{ + ListFlavorsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListFlavorsRequest) (*mongodbflex.ListFlavorsResponse, error) { + c.listFlavorsCalled = true + if c.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return utils.Ptr(mongodbflex.ListFlavorsResponse{ + Flavors: []mongodbflex.InstanceFlavor{}, + }), nil + }), + ListVersionsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListVersionsRequest) (*mongodbflex.ListVersionsResponse, error) { + c.listVersionsCalled = true + if c.listVersionsFails { + return nil, fmt.Errorf("list versions failed") + } + return utils.Ptr(mongodbflex.ListVersionsResponse{ + Versions: []string{}, + }), nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListStoragesRequest) (*mongodbflex.ListStoragesResponse, error) { + c.listStoragesCalled = true + if c.listStoragesFails { + return nil, fmt.Errorf("list storages failed") + } + return utils.Ptr(mongodbflex.ListStoragesResponse{ + StorageClasses: []string{}, + StorageRange: &mongodbflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, + }), nil + }), } - return utils.Ptr(mongodbflex.ListStoragesResponse{ - StorageClasses: utils.Ptr([]string{}), - StorageRange: &mongodbflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }), nil } func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -105,6 +106,7 @@ func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -167,46 +169,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -216,9 +179,7 @@ func TestBuildAndExecuteRequest(t *testing.T) { description string model *inputModel isValid bool - listFlavorsFails bool - listVersionsFails bool - listStoragesFails bool + mockClientSettings mockSettings expectListFlavorsCalled bool expectListVersionsCalled bool expectListStoragesCalled bool @@ -261,28 +222,34 @@ func TestBuildAndExecuteRequest(t *testing.T) { expectListStoragesCalled: true, }, { - description: "list flavors fails", - model: fixtureInputModelAllTrue(), - isValid: false, - listFlavorsFails: true, + description: "list flavors fails", + model: fixtureInputModelAllTrue(), + isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, expectListFlavorsCalled: true, expectListVersionsCalled: false, expectListStoragesCalled: false, }, { - description: "list versions fails", - model: fixtureInputModelAllTrue(), - isValid: false, - listVersionsFails: true, + description: "list versions fails", + model: fixtureInputModelAllTrue(), + isValid: false, + mockClientSettings: mockSettings{ + listVersionsFails: true, + }, expectListFlavorsCalled: true, expectListVersionsCalled: true, expectListStoragesCalled: false, }, { - description: "list storages fails", - model: fixtureInputModelAllTrue(), - isValid: false, - listStoragesFails: true, + description: "list storages fails", + model: fixtureInputModelAllTrue(), + isValid: false, + mockClientSettings: mockSettings{ + listStoragesFails: true, + }, expectListFlavorsCalled: true, expectListVersionsCalled: true, expectListStoragesCalled: true, @@ -291,16 +258,9 @@ func TestBuildAndExecuteRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := &print.Printer{} - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - p.Cmd = cmd - client := &mongoDBFlexClientMocked{ - listFlavorsFails: tt.listFlavorsFails, - listVersionsFails: tt.listVersionsFails, - listStoragesFails: tt.listStoragesFails, - } + params := testparams.NewTestParams() - err := buildAndExecuteRequest(testCtx, p, tt.model, client) + err := buildAndExecuteRequest(testCtx, params.Printer, tt.model, newAPIClientMock(&tt.mockClientSettings)) if err != nil && tt.isValid { t.Fatalf("error building and executing request: %v", err) } @@ -311,14 +271,14 @@ func TestBuildAndExecuteRequest(t *testing.T) { return } - if tt.expectListFlavorsCalled != client.listFlavorsCalled { - t.Fatalf("expected listFlavorsCalled to be %v, got %v", tt.expectListFlavorsCalled, client.listFlavorsCalled) + if tt.expectListFlavorsCalled != (tt.mockClientSettings).listFlavorsCalled { + t.Fatalf("expected listFlavorsCalled to be %v, got %v", tt.expectListFlavorsCalled, (tt.mockClientSettings).listFlavorsCalled) } - if tt.expectListVersionsCalled != client.listVersionsCalled { - t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, client.listVersionsCalled) + if tt.expectListVersionsCalled != (tt.mockClientSettings).listVersionsCalled { + t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, (tt.mockClientSettings).listVersionsCalled) } - if tt.expectListStoragesCalled != client.listStoragesCalled { - t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, client.listStoragesCalled) + if tt.expectListStoragesCalled != (tt.mockClientSettings).listStoragesCalled { + t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, (tt.mockClientSettings).listStoragesCalled) } }) } @@ -406,11 +366,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.inputModel, tt.args.flavors, tt.args.versions, tt.args.storages); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.inputModel, tt.args.flavors, tt.args.versions, tt.args.storages); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -455,11 +414,10 @@ func TestOutputResultAsTable(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResultAsTable(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + if err := outputResultAsTable(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { t.Errorf("outputResultAsTable() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/user/create/create.go b/internal/cmd/mongodbflex/user/create/create.go index 2ddf76d34..692e7abb8 100644 --- a/internal/cmd/mongodbflex/user/create/create.go +++ b/internal/cmd/mongodbflex/user/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,18 +18,22 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" mongodbflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) const ( instanceIdFlag = "instance-id" usernameFlag = "username" databaseFlag = "database" - roleFlag = "role" ) var ( rolesDefault = []string{"read"} + roleFlag = flags.StringEnumSliceFlag( + "role", + []string{"read", "readWrite", "readAnyDatabase", "readWriteAnyDatabase", "stackitAdmin"}, + "Roles of the user. The \"readAnyDatabase\", \"readWriteAnyDatabase\" and \"stackitAdmin\" roles will always be created in the admin database.", + flags.DefaultValues("read"), + ) ) type inputModel struct { @@ -40,7 +45,7 @@ type inputModel struct { Roles *[]string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a MongoDB Flex user", @@ -59,9 +64,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit mongodbflex user create --instance-id xxx --role read --database default"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,18 +77,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -103,18 +106,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - roleOptions := []string{"read", "readWrite", "readWriteAnyDatabase"} - cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") cmd.Flags().String(usernameFlag, "", "Username of the user. If not specified, a random username will be assigned") cmd.Flags().String(databaseFlag, "", "The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it") - cmd.Flags().Var(flags.EnumSliceFlag(false, rolesDefault, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q", roleOptions)) + roleFlag.Register(cmd) err := flags.MarkFlagsRequired(cmd, instanceIdFlag, databaseFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -125,27 +126,19 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), Username: flags.FlagToStringPointer(p, cmd, usernameFlag), Database: flags.FlagToStringPointer(p, cmd, databaseFlag), - Roles: flags.FlagWithDefaultToStringSlicePointer(p, cmd, roleFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Roles: roleFlag.Ptr(), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiCreateUserRequest { - req := apiClient.CreateUser(ctx, model.ProjectId, model.InstanceId, model.Region) + req := apiClient.DefaultAPI.CreateUser(ctx, model.ProjectId, model.InstanceId, model.Region) req = req.CreateUserPayload(mongodbflex.CreateUserPayload{ Username: model.Username, - Database: model.Database, - Roles: model.Roles, + Database: *model.Database, + Roles: *model.Roles, }) return req } @@ -155,33 +148,16 @@ func outputResult(p *print.Printer, outputFormat, instanceLabel string, user *mo return fmt.Errorf("user is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, user, func() error { p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, utils.PtrString(user.Id)) p.Outputf("Username: %s\n", utils.PtrString(user.Username)) p.Outputf("Password: %s\n", utils.PtrString(user.Password)) - p.Outputf("Roles: %v\n", utils.PtrString(user.Roles)) + p.Outputf("Roles: %v\n", user.Roles) p.Outputf("Database: %s\n", utils.PtrString(user.Database)) p.Outputf("Host: %s\n", utils.PtrString(user.Host)) p.Outputf("Port: %s\n", utils.PtrString(user.Port)) p.Outputf("URI: %s\n", utils.PtrString(user.Uri)) return nil - } + }) } diff --git a/internal/cmd/mongodbflex/user/create/create_test.go b/internal/cmd/mongodbflex/user/create/create_test.go index db666260b..937b32344 100644 --- a/internal/cmd/mongodbflex/user/create/create_test.go +++ b/internal/cmd/mongodbflex/user/create/create_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -23,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -34,7 +33,6 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st instanceIdFlag: testInstanceId, usernameFlag: "johndoe", databaseFlag: "default", - roleFlag: "read", } for _, mod := range mods { mod(flagValues) @@ -61,11 +59,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiCreateUserRequest)) mongodbflex.ApiCreateUserRequest { - request := testClient.CreateUser(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.CreateUser(testCtx, testProjectId, testInstanceId, testRegion) request = request.CreateUserPayload(mongodbflex.CreateUserPayload{ Username: utils.Ptr("johndoe"), - Database: utils.Ptr("default"), - Roles: utils.Ptr([]string{"read"}), + Database: "default", + Roles: []string{"read"}, }) for _, mod := range mods { @@ -77,6 +75,7 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiCreateUserRequest)) mon func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -148,7 +147,7 @@ func TestParseInput(t *testing.T) { { description: "roles missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, roleFlag) + delete(flagValues, roleFlag.Name()) }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -158,7 +157,7 @@ func TestParseInput(t *testing.T) { { description: "invalid role", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[roleFlag] = "invalid-role" + flagValues[roleFlag.Name()] = "invalid-role" }), isValid: false, }, @@ -166,48 +165,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -229,8 +187,8 @@ func TestBuildRequest(t *testing.T) { model.Username = nil }), expectedRequest: fixtureRequest().CreateUserPayload(mongodbflex.CreateUserPayload{ - Database: utils.Ptr("default"), - Roles: utils.Ptr([]string{"read"}), + Database: "default", + Roles: []string{"read"}, }), }, } @@ -241,7 +199,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -274,11 +232,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/user/delete/delete.go b/internal/cmd/mongodbflex/user/delete/delete.go index 05e72686b..75477948b 100644 --- a/internal/cmd/mongodbflex/user/delete/delete.go +++ b/internal/cmd/mongodbflex/user/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -32,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", userIdArg), Short: "Deletes a MongoDB Flex user", @@ -59,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := mongodbflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId, model.Region) + userLabel, err := mongodbflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -115,19 +114,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiDeleteUserRequest { - req := apiClient.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) return req } diff --git a/internal/cmd/mongodbflex/user/delete/delete_test.go b/internal/cmd/mongodbflex/user/delete/delete_test.go index 1a20490f1..d14df20cc 100644 --- a/internal/cmd/mongodbflex/user/delete/delete_test.go +++ b/internal/cmd/mongodbflex/user/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -21,7 +20,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -65,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiDeleteUserRequest)) mongodbflex.ApiDeleteUserRequest { - request := testClient.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) for _, mod := range mods { mod(&request) } @@ -169,54 +168,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -240,7 +192,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mongodbflex/user/describe/describe.go b/internal/cmd/mongodbflex/user/describe/describe.go index 410896fc9..f6d85cbb3 100644 --- a/internal/cmd/mongodbflex/user/describe/describe.go +++ b/internal/cmd/mongodbflex/user/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", userIdArg), Short: "Shows details of a MongoDB Flex user", @@ -101,48 +100,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetUserRequest { - req := apiClient.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, user mongodbflex.InstanceResponseUser) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, user, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(user.Id)) table.AddSeparator() table.AddRow("USERNAME", utils.PtrString(user.Username)) table.AddSeparator() - table.AddRow("ROLES", utils.PtrString(user.Roles)) + table.AddRow("ROLES", user.Roles) table.AddSeparator() table.AddRow("DATABASE", utils.PtrString(user.Database)) table.AddSeparator() @@ -156,5 +130,5 @@ func outputResult(p *print.Printer, outputFormat string, user mongodbflex.Instan } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/user/describe/describe_test.go b/internal/cmd/mongodbflex/user/describe/describe_test.go index 5315f8b8a..8a67beda2 100644 --- a/internal/cmd/mongodbflex/user/describe/describe_test.go +++ b/internal/cmd/mongodbflex/user/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -21,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -65,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiGetUserRequest)) mongodbflex.ApiGetUserRequest { - request := testClient.GetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.GetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) for _, mod := range mods { mod(&request) } @@ -169,54 +169,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -240,7 +193,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -272,11 +225,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instanceResponseUser); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceResponseUser); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/user/list/list.go b/internal/cmd/mongodbflex/user/list/list.go index d9e947e9c..47c482ca3 100644 --- a/internal/cmd/mongodbflex/user/list/list.go +++ b/internal/cmd/mongodbflex/user/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all MongoDB Flex users of an instance", @@ -51,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit mongodbflex user list --instance-id xxx --limit 10"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,23 +69,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get MongoDB Flex users: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId, model.Region) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = *model.InstanceId - } - params.Printer.Info("No users found for instance %q\n", instanceLabel) - return nil + users := resp.Items + + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, *model.InstanceId, model.Region) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = *model.InstanceId } - users := *resp.Items // Truncate output if model.Limit != nil && len(users) > int(*model.Limit) { users = users[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, users) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, users) }, } @@ -102,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -122,42 +118,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiListUsersRequest { - req := apiClient.ListUsers(ctx, model.ProjectId, *model.InstanceId, model.Region) + req := apiClient.DefaultAPI.ListUsers(ctx, model.ProjectId, *model.InstanceId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, users []mongodbflex.ListUser) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(users, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex user list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, users []mongodbflex.ListUser) error { + return p.OutputResult(outputFormat, users, func() error { + if len(users) == 0 { + p.Outputf("No users found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex user list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "USERNAME") for i := range users { @@ -173,5 +149,5 @@ func outputResult(p *print.Printer, outputFormat string, users []mongodbflex.Lis } return nil - } + }) } diff --git a/internal/cmd/mongodbflex/user/list/list_test.go b/internal/cmd/mongodbflex/user/list/list_test.go index 45d28d41d..d249268b3 100644 --- a/internal/cmd/mongodbflex/user/list/list_test.go +++ b/internal/cmd/mongodbflex/user/list/list_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -23,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -57,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiListUsersRequest)) mongodbflex.ApiListUsersRequest { - request := testClient.ListUsers(testCtx, testProjectId, testInstanceId, testRegion) + request := testClient.DefaultAPI.ListUsers(testCtx, testProjectId, testInstanceId, testRegion) for _, mod := range mods { mod(&request) } @@ -67,6 +66,7 @@ func fixtureRequest(mods ...func(request *mongodbflex.ApiListUsersRequest)) mong func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -135,48 +135,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +159,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -211,8 +170,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - users []mongodbflex.ListUser + outputFormat string + instanceLabel string + users []mongodbflex.ListUser } tests := []struct { name string @@ -239,11 +199,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.users); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.users); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/user/reset-password/reset_password.go b/internal/cmd/mongodbflex/user/reset-password/reset_password.go index c58dfd020..8c21e3f6d 100644 --- a/internal/cmd/mongodbflex/user/reset-password/reset_password.go +++ b/internal/cmd/mongodbflex/user/reset-password/reset_password.go @@ -2,11 +2,10 @@ package resetpassword import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("reset-password %s", userIdArg), Short: "Resets the password of a MongoDB Flex user", @@ -61,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := mongodbflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId, model.Region) + userLabel, err := mongodbflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -117,20 +114,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiResetUserRequest { - req := apiClient.ResetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.ResetUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) return req } @@ -139,28 +128,11 @@ func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel strin return fmt.Errorf("user is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal MongoDB Flex reset password: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal MongoDB Flex reset password: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, user, func() error { p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel) p.Outputf("Username: %s\n", utils.PtrString(user.Username)) p.Outputf("New password: %s\n", utils.PtrString(user.Password)) p.Outputf("New URI: %s\n", utils.PtrString(user.Uri)) return nil - } + }) } diff --git a/internal/cmd/mongodbflex/user/reset-password/reset_password_test.go b/internal/cmd/mongodbflex/user/reset-password/reset_password_test.go index 349baec27..6843186b0 100644 --- a/internal/cmd/mongodbflex/user/reset-password/reset_password_test.go +++ b/internal/cmd/mongodbflex/user/reset-password/reset_password_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -21,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -65,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiResetUserRequest)) mongodbflex.ApiResetUserRequest { - request := testClient.ResetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.ResetUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) for _, mod := range mods { mod(&request) } @@ -169,54 +169,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -240,7 +193,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -274,11 +227,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.userLabel, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.userLabel, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/mongodbflex/user/update/update.go b/internal/cmd/mongodbflex/user/update/update.go index 5f192f377..d8e67fc32 100644 --- a/internal/cmd/mongodbflex/user/update/update.go +++ b/internal/cmd/mongodbflex/user/update/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -24,7 +25,12 @@ const ( instanceIdFlag = "instance-id" databaseFlag = "database" - roleFlag = "role" +) + +var roleFlag = flags.StringEnumSliceFlag( + "role", + []string{"read", "readWrite", "readAnyDatabase", "readWriteAnyDatabase", "stackitAdmin"}, + "Roles of the user. The \"readAnyDatabase\", \"readWriteAnyDatabase\" and \"stackitAdmin\" roles will always be created in the admin database.", ) type inputModel struct { @@ -33,10 +39,10 @@ type inputModel struct { InstanceId string UserId string Database *string - Roles *[]string + Roles []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", userIdArg), Short: "Updates a MongoDB Flex user", @@ -60,24 +66,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId, model.Region) + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := mongodbflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId, model.Region) + userLabel, err := mongodbflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -97,12 +101,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - roleOptions := []string{"read", "readWrite", "readWriteAnyDatabase"} - cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") cmd.Flags().String(databaseFlag, "", "The database inside the MongoDB instance that the user has access to. If it does not exist, it will be created once the user writes to it") - cmd.Flags().Var(flags.EnumSliceFlag(false, nil, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q", roleOptions)) + roleFlag.Register(cmd) + cmd.MarkFlagsOneRequired(databaseFlag, roleFlag.Name()) err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) } @@ -116,11 +119,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } database := flags.FlagToStringPointer(p, cmd, databaseFlag) - roles := flags.FlagToStringSlicePointer(p, cmd, roleFlag) - - if database == nil && roles == nil { - return nil, &errors.EmptyUpdateError{} - } + roles := roleFlag.Get() model := inputModel{ GlobalFlagModel: globalFlags, @@ -130,20 +129,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Roles: roles, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiPartialUpdateUserRequest { - req := apiClient.PartialUpdateUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) + req := apiClient.DefaultAPI.PartialUpdateUser(ctx, model.ProjectId, model.InstanceId, model.UserId, model.Region) req = req.PartialUpdateUserPayload(mongodbflex.PartialUpdateUserPayload{ Database: model.Database, Roles: model.Roles, diff --git a/internal/cmd/mongodbflex/user/update/update_test.go b/internal/cmd/mongodbflex/user/update/update_test.go index 3f12bda1a..6457e38c8 100644 --- a/internal/cmd/mongodbflex/user/update/update_test.go +++ b/internal/cmd/mongodbflex/user/update/update_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) const ( @@ -22,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &mongodbflex.APIClient{} +var testClient = &mongodbflex.APIClient{DefaultAPI: &mongodbflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -68,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *mongodbflex.ApiPartialUpdateUserRequest)) mongodbflex.ApiPartialUpdateUserRequest { - request := testClient.PartialUpdateUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) + request := testClient.DefaultAPI.PartialUpdateUser(testCtx, testProjectId, testInstanceId, testUserId, testRegion) request = request.PartialUpdateUserPayload(mongodbflex.PartialUpdateUserPayload{ Database: utils.Ptr("default"), }) @@ -110,11 +109,11 @@ func TestParseInput(t *testing.T) { description: "update roles", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[roleFlag] = "read" + flagValues[roleFlag.Name()] = "read" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Roles = utils.Ptr([]string{"read"}) + model.Roles = []string{"read"} }), }, { @@ -184,7 +183,7 @@ func TestParseInput(t *testing.T) { description: "invalid role", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[roleFlag] = "invalid-role" + flagValues[roleFlag.Name()] = "invalid-role" }), isValid: false, }, @@ -193,7 +192,7 @@ func TestParseInput(t *testing.T) { argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, databaseFlag) - delete(flagValues, roleFlag) + delete(flagValues, roleFlag.Name()) }), isValid: false, }, @@ -201,54 +200,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -268,10 +220,10 @@ func TestBuildRequest(t *testing.T) { description: "update roles only", model: fixtureInputModel(func(model *inputModel) { model.Database = nil - model.Roles = &[]string{"default"} + model.Roles = []string{"default"} }), expectedRequest: fixtureRequest().PartialUpdateUserPayload(mongodbflex.PartialUpdateUserPayload{ - Roles: &[]string{"default"}, + Roles: []string{"default"}, }), }, } @@ -282,7 +234,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, mongodbflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/mongodbflex/user/user.go b/internal/cmd/mongodbflex/user/user.go index 0e0600ed6..ed76dffbf 100644 --- a/internal/cmd/mongodbflex/user/user.go +++ b/internal/cmd/mongodbflex/user/user.go @@ -7,14 +7,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/user/list" resetpassword "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/user/reset-password" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/user/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "user", Short: "Provides functionality for MongoDB Flex users", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/network-area/create/create.go b/internal/cmd/network-area/create/create.go index 870e6a439..9b30bd871 100644 --- a/internal/cmd/network-area/create/create.go +++ b/internal/cmd/network-area/create/create.go @@ -2,11 +2,12 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -16,37 +17,31 @@ import ( rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) const ( - nameFlag = "name" - organizationIdFlag = "organization-id" - dnsNameServersFlag = "dns-name-servers" - networkRangesFlag = "network-ranges" - transferNetworkFlag = "transfer-network" - defaultPrefixLengthFlag = "default-prefix-length" - maxPrefixLengthFlag = "max-prefix-length" - minPrefixLengthFlag = "min-prefix-length" - labelFlag = "labels" + nameFlag = "name" + organizationIdFlag = "organization-id" + labelFlag = "labels" ) type inputModel struct { *globalflags.GlobalFlagModel - Name *string - OrganizationId *string - DnsNameServers *[]string - NetworkRanges *[]string - TransferNetwork *string - DefaultPrefixLength *int64 - MaxPrefixLength *int64 - MinPrefixLength *int64 - Labels *map[string]string + Name string + OrganizationId string + Labels map[string]any +} + +// NetworkAreaResponses is a workaround, to keep the two responses of the iaas v2 api together for the json and yaml output +// Should be removed when the deprecated flags are removed +type NetworkAreaResponses struct { + NetworkArea iaas.NetworkArea `json:"network_area"` + RegionalArea *iaas.RegionalArea `json:"regional_area"` } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a STACKIT Network Area (SNA)", @@ -54,25 +49,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network`, - `$ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24"`, - ), - examples.NewExample( - `Create a network area with name "network-area-2" in organization with ID "xxx" with network ranges, transfer network and DNS name server`, - `$ stackit network-area create --name network-area-2 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --dns-name-servers "1.1.1.1"`, + `Create a network area with name "network-area-1" in organization with ID "xxx"`, + `$ stackit network-area create --name network-area-1 --organization-id xxx"`, ), examples.NewExample( - `Create a network area with name "network-area-3" in organization with ID "xxx" with network ranges, transfer network and additional options`, - `$ stackit network-area create --name network-area-3 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --default-prefix-length 25 --max-prefix-length 29 --min-prefix-length 24`, - ), - examples.NewExample( - `Create a network area with name "network-area-1" in organization with ID "xxx" with network ranges and a transfer network and labels "key=value,key1=value1"`, - `$ stackit network-area create --name network-area-1 --organization-id xxx --network-ranges "1.1.1.0/24,192.123.1.0/24" --transfer-network "192.160.0.0/24" --labels key=value,key1=value1`, + `Create a network area with name "network-area-1" in organization with ID "xxx" with labels "key=value,key1=value1"`, + `$ stackit network-area create --name network-area-1 --organization-id xxx --labels key=value,key1=value1`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -86,23 +73,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { var orgLabel string rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) if err == nil { - orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) + orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient.DefaultAPI, model.OrganizationId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) - orgLabel = *model.OrganizationId + orgLabel = model.OrganizationId } else if orgLabel == "" { - orgLabel = *model.OrganizationId + orgLabel = model.OrganizationId } } else { params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a network area for organization %q?", orgLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a network area for organization %q?", orgLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,8 +96,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create network area: %w", err) } + if resp == nil || resp.Id == nil { + return fmt.Errorf("create network area: empty response") + } + + responses := &NetworkAreaResponses{ + NetworkArea: *resp, + } - return outputResult(params.Printer, model.OutputFormat, orgLabel, resp) + return outputResult(params.Printer, model.OutputFormat, orgLabel, responses) }, } configureFlags(cmd) @@ -122,97 +114,50 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(nameFlag, "n", "", "Network area name") cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") - cmd.Flags().StringSlice(dnsNameServersFlag, nil, "List of DNS name server IPs") - cmd.Flags().Var(flags.CIDRSliceFlag(), networkRangesFlag, "List of network ranges") - cmd.Flags().Var(flags.CIDRFlag(), transferNetworkFlag, "Transfer network in CIDR notation") - cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") - cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") - cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...'") - err := flags.MarkFlagsRequired(cmd, nameFlag, organizationIdFlag, networkRangesFlag, transferNetworkFlag) + err := flags.MarkFlagsRequired(cmd, nameFlag, organizationIdFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := inputModel{ - GlobalFlagModel: globalFlags, - Name: flags.FlagToStringPointer(p, cmd, nameFlag), - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, dnsNameServersFlag), - NetworkRanges: flags.FlagToStringSlicePointer(p, cmd, networkRangesFlag), - TransferNetwork: flags.FlagToStringPointer(p, cmd, transferNetworkFlag), - DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, defaultPrefixLengthFlag), - MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, maxPrefixLengthFlag), - MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, minPrefixLengthFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringValue(p, cmd, nameFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRequest { - req := apiClient.CreateNetworkArea(ctx, *model.OrganizationId) - - networkRanges := make([]iaas.NetworkRange, len(*model.NetworkRanges)) - for i, networkRange := range *model.NetworkRanges { - networkRanges[i] = iaas.NetworkRange{ - Prefix: utils.Ptr(networkRange), - } - } + req := apiClient.DefaultAPI.CreateNetworkArea(ctx, model.OrganizationId) payload := iaas.CreateNetworkAreaPayload{ Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - AddressFamily: &iaas.CreateAreaAddressFamily{ - Ipv4: &iaas.CreateAreaIPv4{ - DefaultNameservers: model.DnsNameServers, - NetworkRanges: utils.Ptr(networkRanges), - TransferNetwork: model.TransferNetwork, - DefaultPrefixLen: model.DefaultPrefixLength, - MaxPrefixLen: model.MaxPrefixLength, - MinPrefixLen: model.MinPrefixLength, - }, - }, + Labels: model.Labels, } return req.CreateNetworkAreaPayload(payload) } -func outputResult(p *print.Printer, outputFormat, orgLabel string, networkArea *iaas.NetworkArea) error { - if networkArea == nil { +func outputResult(p *print.Printer, outputFormat, orgLabel string, responses *NetworkAreaResponses) error { + if responses == nil { return fmt.Errorf("network area is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkArea, "", " ") - if err != nil { - return fmt.Errorf("marshal network area: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkArea, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network area: %w", err) - } - p.Outputln(string(details)) + prettyOutputFunc := func() error { + p.Outputf("Created STACKIT Network Area for organization %q.\nNetwork area ID: %s\n", orgLabel, utils.PtrString(responses.NetworkArea.Id)) return nil - default: - p.Outputf("Created STACKIT Network Area for organization %q.\nNetwork area ID: %s\n", orgLabel, utils.PtrString(networkArea.AreaId)) - return nil } + // If RegionalArea is NOT set in the response, then no deprecated Flags were set. + // In this case, only the response of NetworkArea should be printed in JSON and yaml output, to avoid breaking changes after the deprecated fields are removed + if responses.RegionalArea == nil { + return p.OutputResult(outputFormat, responses.NetworkArea, prettyOutputFunc) + } + return p.OutputResult(outputFormat, responses, prettyOutputFunc) } diff --git a/internal/cmd/network-area/create/create_test.go b/internal/cmd/network-area/create/create_test.go index 7b9d235cd..6c772b42c 100644 --- a/internal/cmd/network-area/create/create_test.go +++ b/internal/cmd/network-area/create/create_test.go @@ -4,35 +4,38 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" + testName = "example-network-area-name" + testTransferNetwork = "100.0.0.0/24" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 26 + testMinPrefixLength int64 = 24 ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} - +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - nameFlag: "example-network-area-name", - organizationIdFlag: testOrgId, - dnsNameServersFlag: "1.1.1.0,1.1.2.0", - networkRangesFlag: "192.0.0.0/24,102.0.0.0/24", - transferNetworkFlag: "100.0.0.0/24", - defaultPrefixLengthFlag: "24", - maxPrefixLengthFlag: "24", - minPrefixLengthFlag: "24", - labelFlag: "key=value", + globalflags.RegionFlag: testRegion, + + nameFlag: testName, + organizationIdFlag: testOrgId, + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -44,18 +47,13 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-area-name"), - OrganizationId: utils.Ptr(testOrgId), - DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - NetworkRanges: utils.Ptr([]string{"192.0.0.0/24", "102.0.0.0/24"}), - TransferNetwork: utils.Ptr("100.0.0.0/24"), - DefaultPrefixLength: utils.Ptr(int64(24)), - MaxPrefixLength: utils.Ptr(int64(24)), - MinPrefixLength: utils.Ptr(int64(24)), - Labels: utils.Ptr(map[string]string{ + Name: "example-network-area-name", + OrganizationId: testOrgId, + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -64,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRequest)) iaas.ApiCreateNetworkAreaRequest { - request := testClient.CreateNetworkArea(testCtx, testOrgId) + request := testClient.DefaultAPI.CreateNetworkArea(testCtx, testOrgId) request = request.CreateNetworkAreaPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -74,26 +72,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRequest)) iaa func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaPayload)) iaas.CreateNetworkAreaPayload { payload := iaas.CreateNetworkAreaPayload{ - Name: utils.Ptr("example-network-area-name"), - Labels: utils.Ptr(map[string]interface{}{ + Name: "example-network-area-name", + Labels: map[string]any{ "key": "value", - }), - AddressFamily: &iaas.CreateAreaAddressFamily{ - Ipv4: &iaas.CreateAreaIPv4{ - DefaultNameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - NetworkRanges: &[]iaas.NetworkRange{ - { - Prefix: utils.Ptr("192.0.0.0/24"), - }, - { - Prefix: utils.Ptr("102.0.0.0/24"), - }, - }, - TransferNetwork: utils.Ptr("100.0.0.0/24"), - DefaultPrefixLen: utils.Ptr(int64(24)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(24)), - }, }, } for _, mod := range mods { @@ -105,6 +86,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaPayload)) iaas.C func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -116,22 +98,6 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(), }, - { - description: "required only", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, dnsNameServersFlag) - delete(flagValues, defaultPrefixLengthFlag) - delete(flagValues, maxPrefixLengthFlag) - delete(flagValues, minPrefixLengthFlag) - }), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.DnsNameServers = nil - model.DefaultPrefixLength = nil - model.MaxPrefixLength = nil - model.MinPrefixLength = nil - }), - }, { description: "name missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { @@ -139,20 +105,6 @@ func TestParseInput(t *testing.T) { }), isValid: false, }, - { - description: "network ranges missing", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, networkRangesFlag) - }), - isValid: false, - }, - { - description: "transfer network missing", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, transferNetworkFlag) - }), - isValid: false, - }, { description: "no values", flagValues: map[string]string{}, @@ -193,46 +145,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -256,7 +169,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -269,7 +182,7 @@ func Test_outputResult(t *testing.T) { type args struct { outputFormat string orgLabel string - networkArea *iaas.NetworkArea + responses *NetworkAreaResponses } tests := []struct { name string @@ -281,19 +194,27 @@ func Test_outputResult(t *testing.T) { args: args{}, wantErr: true, }, + { + name: "set empty response", + args: args{ + responses: &NetworkAreaResponses{}, + }, + wantErr: false, + }, { name: "set empty network area", args: args{ - networkArea: &iaas.NetworkArea{}, + responses: &NetworkAreaResponses{ + NetworkArea: iaas.NetworkArea{}, + }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.orgLabel, tt.args.networkArea); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.orgLabel, tt.args.responses); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/delete/delete.go b/internal/cmd/network-area/delete/delete.go index b545fca4b..e3e984d6e 100644 --- a/internal/cmd/network-area/delete/delete.go +++ b/internal/cmd/network-area/delete/delete.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -12,12 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - - "github.com/spf13/cobra" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { AreaId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", areaIdArg), Short: "Deletes a STACKIT Network Area (SNA)", @@ -59,18 +58,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, model.AreaId) + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, *model.OrganizationId, model.AreaId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) networkAreaLabel = model.AreaId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete network area %q?", networkAreaLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete network area %q?", networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -80,22 +77,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("delete network area: %w", err) } - // Wait for async operation, if async mode not enabled - if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting network area") - _, err = wait.DeleteNetworkAreaWaitHandler(ctx, apiClient, *model.OrganizationId, model.AreaId).WaitWithContext(ctx) - if err != nil { - return fmt.Errorf("wait for network area deletion: %w", err) - } - s.Stop() - } - - operationState := "Deleted" - if model.Async { - operationState = "Triggered deletion of" - } - params.Printer.Info("%s STACKIT Network Area %q\n", operationState, networkAreaLabel) + params.Printer.Outputf("Deleted STACKIT Network Area %q\n", networkAreaLabel) return nil }, } @@ -121,18 +103,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu AreaId: areaId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkAreaRequest { - return apiClient.DeleteNetworkArea(ctx, *model.OrganizationId, model.AreaId) + return apiClient.DefaultAPI.DeleteNetworkArea(ctx, *model.OrganizationId, model.AreaId) } diff --git a/internal/cmd/network-area/delete/delete_test.go b/internal/cmd/network-area/delete/delete_test.go index 4b8077590..62db3491a 100644 --- a/internal/cmd/network-area/delete/delete_test.go +++ b/internal/cmd/network-area/delete/delete_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrganizationId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -56,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRequest)) iaas.ApiDeleteNetworkAreaRequest { - request := testClient.DeleteNetworkArea(testCtx, testOrganizationId, testNetworkAreaId) + request := testClient.DefaultAPI.DeleteNetworkArea(testCtx, testOrganizationId, testNetworkAreaId) for _, mod := range mods { mod(&request) } @@ -136,54 +135,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -207,7 +159,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/network-area/describe/describe.go b/internal/cmd/network-area/describe/describe.go index e086f3dd9..b70dd18a1 100644 --- a/internal/cmd/network-area/describe/describe.go +++ b/internal/cmd/network-area/describe/describe.go @@ -2,13 +2,14 @@ package describe import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -18,7 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -36,7 +36,7 @@ type inputModel struct { ShowAttachedProjects bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", areaIdArg), Short: "Shows details of a STACKIT Network Area", @@ -79,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { var projects []string if model.ShowAttachedProjects { - projects, err = iaasUtils.ListAttachedProjects(ctx, apiClient, *model.OrganizationId, model.AreaId) + projects, err = iaasUtils.ListAttachedProjects(ctx, apiClient.DefaultAPI, *model.OrganizationId, model.AreaId) if err != nil && errors.Is(err, iaasUtils.ErrItemsNil) { projects = []string{} } else if err != nil { @@ -114,101 +114,28 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ShowAttachedProjects: flags.FlagToBoolValue(p, cmd, showAttachedProjectsFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkAreaRequest { - return apiClient.GetNetworkArea(ctx, *model.OrganizationId, model.AreaId) + return apiClient.DefaultAPI.GetNetworkArea(ctx, *model.OrganizationId, model.AreaId) } func outputResult(p *print.Printer, outputFormat string, networkArea *iaas.NetworkArea, attachedProjects []string) error { if networkArea == nil { return fmt.Errorf("network area is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkArea, "", " ") - if err != nil { - return fmt.Errorf("marshal network area: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkArea, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network area: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - var routes []string - var networkRanges []string - - if networkArea.Ipv4 != nil { - if networkArea.Ipv4.Routes != nil { - for _, route := range *networkArea.Ipv4.Routes { - routes = append(routes, fmt.Sprintf("next hop: %s\nprefix: %s", *route.Nexthop, *route.Prefix)) - } - } - - if networkArea.Ipv4.NetworkRanges != nil { - for _, networkRange := range *networkArea.Ipv4.NetworkRanges { - networkRanges = append(networkRanges, *networkRange.Prefix) - } - } - } + return p.OutputResult(outputFormat, networkArea, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(networkArea.AreaId)) + table.AddRow("ID", utils.PtrString(networkArea.Id)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(networkArea.Name)) + table.AddRow("NAME", networkArea.Name) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(networkArea.State)) - table.AddSeparator() - if len(networkRanges) > 0 { - table.AddRow("NETWORK RANGES", strings.Join(networkRanges, ",")) - } - table.AddSeparator() - for i, route := range routes { - table.AddRow(fmt.Sprintf("STATIC ROUTE %d", i+1), route) - table.AddSeparator() - } - if networkArea.Ipv4 != nil { - if networkArea.Ipv4.TransferNetwork != nil { - table.AddRow("TRANSFER RANGE", *networkArea.Ipv4.TransferNetwork) - table.AddSeparator() - } - if networkArea.Ipv4.DefaultNameservers != nil && len(*networkArea.Ipv4.DefaultNameservers) > 0 { - table.AddRow("DNS NAME SERVERS", strings.Join(*networkArea.Ipv4.DefaultNameservers, ",")) - table.AddSeparator() - } - if networkArea.Ipv4.DefaultPrefixLen != nil { - table.AddRow("DEFAULT PREFIX LENGTH", *networkArea.Ipv4.DefaultPrefixLen) - table.AddSeparator() - } - if networkArea.Ipv4.MaxPrefixLen != nil { - table.AddRow("MAX PREFIX LENGTH", *networkArea.Ipv4.MaxPrefixLen) - table.AddSeparator() - } - if networkArea.Ipv4.MinPrefixLen != nil { - table.AddRow("MIN PREFIX LENGTH", *networkArea.Ipv4.MinPrefixLen) - table.AddSeparator() - } - } - if networkArea.Labels != nil && len(*networkArea.Labels) > 0 { + if len(networkArea.Labels) > 0 { var labels []string - for key, value := range *networkArea.Labels { + for key, value := range networkArea.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -221,11 +148,15 @@ func outputResult(p *print.Printer, outputFormat string, networkArea *iaas.Netwo table.AddRow("# ATTACHED PROJECTS", utils.PtrString(networkArea.ProjectCount)) table.AddSeparator() } + table.AddRow("CREATED AT", utils.PtrString(networkArea.CreatedAt)) + table.AddSeparator() + table.AddRow("UPDATED AT", utils.PtrString(networkArea.UpdatedAt)) + table.AddSeparator() err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/network-area/describe/describe_test.go b/internal/cmd/network-area/describe/describe_test.go index 7160f6a01..b589cbcf3 100644 --- a/internal/cmd/network-area/describe/describe_test.go +++ b/internal/cmd/network-area/describe/describe_test.go @@ -4,20 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrganizationId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -58,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRequest)) iaas.ApiGetNetworkAreaRequest { - request := testClient.GetNetworkArea(testCtx, testOrganizationId, testNetworkAreaId) + request := testClient.DefaultAPI.GetNetworkArea(testCtx, testOrganizationId, testNetworkAreaId) for _, mod := range mods { mod(&request) } @@ -149,54 +149,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -220,7 +173,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -253,11 +206,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkArea, tt.args.attachedProjects); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkArea, tt.args.attachedProjects); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/list/list.go b/internal/cmd/network-area/list/list.go index aef31e48b..a47cf8ed2 100644 --- a/internal/cmd/network-area/list/list.go +++ b/internal/cmd/network-area/list/list.go @@ -2,14 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,7 +21,6 @@ import ( rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -32,11 +32,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel Limit *int64 - OrganizationId *string + OrganizationId string LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all STACKIT Network Areas (SNA) of an organization", @@ -60,9 +60,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit network-area list --organization-id xxx --label-selector yyy", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -80,31 +80,30 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list network areas: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - var orgLabel string - rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) - if err == nil { - orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) - orgLabel = *model.OrganizationId - } else if orgLabel == "" { - orgLabel = *model.OrganizationId - } - } else { - params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + items := resp.GetItems() + + var orgLabel string + rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) + if err == nil { + orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient.DefaultAPI, model.OrganizationId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) + orgLabel = model.OrganizationId } - params.Printer.Info("No STACKIT Network Areas found for organization %q\n", orgLabel) - return nil + } else { + params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + } + + if orgLabel == "" { + orgLabel = model.OrganizationId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, orgLabel, items) }, } configureFlags(cmd) @@ -120,7 +119,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) if limit != nil && *limit < 1 { @@ -133,65 +132,36 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworkAreasRequest { - req := apiClient.ListNetworkAreas(ctx, *model.OrganizationId) + req := apiClient.DefaultAPI.ListNetworkAreas(ctx, model.OrganizationId) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } return req } -func outputResult(p *print.Printer, outputFormat string, networkAreas []iaas.NetworkArea) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkAreas, "", " ") - if err != nil { - return fmt.Errorf("marshal network area: %w", err) +func outputResult(p *print.Printer, outputFormat, orgLabel string, networkAreas []iaas.NetworkArea) error { + return p.OutputResult(outputFormat, networkAreas, func() error { + if len(networkAreas) == 0 { + p.Outputf("No STACKIT Network Areas found for organization %q\n", orgLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkAreas, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal area: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.SetHeader("ID", "Name", "Status", "Network Ranges", "# Attached Projects") + table.SetHeader("ID", "Name", "# Attached Projects") for _, networkArea := range networkAreas { - networkRanges := "n/a" - if ipv4 := networkArea.Ipv4; ipv4 != nil { - if netRange := ipv4.NetworkRanges; netRange != nil { - networkRanges = fmt.Sprint(len(*netRange)) - } - } - table.AddRow( - utils.PtrString(networkArea.AreaId), - utils.PtrString(networkArea.Name), - utils.PtrString(networkArea.State), - networkRanges, + utils.PtrString(networkArea.Id), + networkArea.Name, utils.PtrString(networkArea.ProjectCount), ) table.AddSeparator() @@ -199,5 +169,5 @@ func outputResult(p *print.Printer, outputFormat string, networkAreas []iaas.Net p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/network-area/list/list_test.go b/internal/cmd/network-area/list/list_test.go index d41ae9ad8..af74284b4 100644 --- a/internal/cmd/network-area/list/list_test.go +++ b/internal/cmd/network-area/list/list_test.go @@ -4,21 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrganizationId = uuid.NewString() var testLabelSelector = "foo=bar" @@ -39,7 +39,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, }, - OrganizationId: &testOrganizationId, + OrganizationId: testOrganizationId, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), } @@ -50,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreasRequest)) iaas.ApiListNetworkAreasRequest { - request := testClient.ListNetworkAreas(testCtx, testOrganizationId) + request := testClient.DefaultAPI.ListNetworkAreas(testCtx, testOrganizationId) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -61,6 +61,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreasRequest)) iaas func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -130,46 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -193,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -205,6 +167,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + orgLabel string networkAreas []iaas.NetworkArea } tests := []struct { @@ -232,11 +195,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreas); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.orgLabel, tt.args.networkAreas); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/network-range/create/create.go b/internal/cmd/network-area/network-range/create/create.go index cdc54b800..5d8ba7fd9 100644 --- a/internal/cmd/network-area/network-range/create/create.go +++ b/internal/cmd/network-area/network-range/create/create.go @@ -2,11 +2,12 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -15,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -30,10 +30,10 @@ type inputModel struct { *globalflags.GlobalFlagModel OrganizationId *string NetworkAreaId *string - NetworkRange *string + NetworkRange string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a network range in a STACKIT Network Area (SNA)", @@ -45,9 +45,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit network-area network-range create --network-area-id xxx --organization-id yyy --network-range "1.1.1.0/24"`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -59,18 +59,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Get network area label - networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, *model.OrganizationId, *model.NetworkAreaId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) networkAreaLabel = *model.NetworkAreaId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a network range for STACKIT Network Area (SNA) %q?", networkAreaLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a network range for STACKIT Network Area (SNA) %q?", networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -80,11 +78,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create network range: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { + if len(resp.Items) == 0 { return fmt.Errorf("empty response from API") } - networkRange, err := iaasUtils.GetNetworkRangeFromAPIResponse(*model.NetworkRange, resp.Items) + networkRange, err := iaasUtils.GetNetworkRangeFromAPIResponse(model.NetworkRange, resp.Items) if err != nil { return err } @@ -105,32 +103,24 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) model := inputModel{ GlobalFlagModel: globalFlags, OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), - NetworkRange: flags.FlagToStringPointer(p, cmd, networkRangeFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + NetworkRange: flags.FlagToStringValue(p, cmd, networkRangeFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRangeRequest { - req := apiClient.CreateNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId) + req := apiClient.DefaultAPI.CreateNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) payload := iaas.CreateNetworkAreaRangePayload{ - Ipv4: &[]iaas.NetworkRange{ + Ipv4: []iaas.NetworkRange{ { Prefix: model.NetworkRange, }, @@ -140,25 +130,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, networkRange iaas.NetworkRange) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkRange, "", " ") - if err != nil { - return fmt.Errorf("marshal network range: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkRange, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network range: %w", err) - } - p.Outputln(string(details)) - + return p.OutputResult(outputFormat, networkRange, func() error { + p.Outputf("Created network range for SNA %q.\nNetwork range ID: %s\n", networkAreaLabel, utils.PtrString(networkRange.Id)) return nil - default: - p.Outputf("Created network range for SNA %q.\nNetwork range ID: %s\n", networkAreaLabel, utils.PtrString(networkRange.NetworkRangeId)) - return nil - } + }) } diff --git a/internal/cmd/network-area/network-range/create/create_test.go b/internal/cmd/network-area/network-range/create/create_test.go index 478854cc2..24fee526b 100644 --- a/internal/cmd/network-area/network-range/create/create_test.go +++ b/internal/cmd/network-area/network-range/create/create_test.go @@ -4,27 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, networkRangeFlag: "1.1.1.0/24", @@ -39,10 +45,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), - NetworkRange: utils.Ptr("1.1.1.0/24"), + NetworkRange: "1.1.1.0/24", } for _, mod := range mods { mod(model) @@ -51,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRangeRequest)) iaas.ApiCreateNetworkAreaRangeRequest { - request := testClient.CreateNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId) + request := testClient.DefaultAPI.CreateNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testRegion) request = request.CreateNetworkAreaRangePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -61,9 +68,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRangeRequest) func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRangePayload)) iaas.CreateNetworkAreaRangePayload { payload := iaas.CreateNetworkAreaRangePayload{ - Ipv4: &[]iaas.NetworkRange{ + Ipv4: []iaas.NetworkRange{ { - Prefix: utils.Ptr("1.1.1.0/24"), + Prefix: "1.1.1.0/24", }, }, } @@ -76,6 +83,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRangePayload)) i func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -145,46 +153,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -208,7 +177,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -241,11 +210,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.networkRange); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.networkRange); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/network-range/delete/delete.go b/internal/cmd/network-area/network-range/delete/delete.go index feec26e6b..b563fa0ae 100644 --- a/internal/cmd/network-area/network-range/delete/delete.go +++ b/internal/cmd/network-area/network-range/delete/delete.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -32,7 +34,7 @@ type inputModel struct { NetworkRangeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", networkRangeIdArg), Short: "Deletes a network range in a STACKIT Network Area (SNA)", @@ -57,12 +59,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, *model.OrganizationId, *model.NetworkAreaId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) networkAreaLabel = *model.NetworkAreaId } - networkRangeLabel, err := iaasUtils.GetNetworkRangePrefix(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId, model.NetworkRangeId) + networkRangeLabel, err := iaasUtils.GetNetworkRangePrefix(ctx, apiClient.DefaultAPI, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.NetworkRangeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network range prefix: %v", err) networkRangeLabel = model.NetworkRangeId @@ -70,12 +72,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { networkRangeLabel = model.NetworkRangeId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete network range %q on STACKIT Network Area (SNA) %q?", networkRangeLabel, networkAreaLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete network range %q on STACKIT Network Area (SNA) %q?", networkRangeLabel, networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -113,19 +113,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu NetworkRangeId: networkRangeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkAreaRangeRequest { - req := apiClient.DeleteNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.NetworkRangeId) + req := apiClient.DefaultAPI.DeleteNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.NetworkRangeId) return req } diff --git a/internal/cmd/network-area/network-range/delete/delete_test.go b/internal/cmd/network-area/network-range/delete/delete_test.go index 5fb112ef6..46d83b37e 100644 --- a/internal/cmd/network-area/network-range/delete/delete_test.go +++ b/internal/cmd/network-area/network-range/delete/delete_test.go @@ -4,21 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -36,6 +39,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, } @@ -49,6 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), @@ -61,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRangeRequest)) iaas.ApiDeleteNetworkAreaRangeRequest { - request := testClient.DeleteNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testNetworkRangeId) + request := testClient.DefaultAPI.DeleteNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testRegion, testNetworkRangeId) for _, mod := range mods { mod(&request) } @@ -156,8 +162,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -189,7 +195,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -227,7 +233,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/network-area/network-range/describe/describe.go b/internal/cmd/network-area/network-range/describe/describe.go index d594ebb64..cf9d5e533 100644 --- a/internal/cmd/network-area/network-range/describe/describe.go +++ b/internal/cmd/network-area/network-range/describe/describe.go @@ -2,11 +2,12 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -15,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -34,7 +34,7 @@ type inputModel struct { NetworkRangeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", networkRangeIdArg), Short: "Shows details of a network range in a STACKIT Network Area (SNA)", @@ -92,20 +92,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu NetworkRangeId: networkRangeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkAreaRangeRequest { - req := apiClient.GetNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.NetworkRangeId) + req := apiClient.DefaultAPI.GetNetworkAreaRange(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.NetworkRangeId) return req } @@ -113,33 +105,17 @@ func outputResult(p *print.Printer, outputFormat string, networkRange *iaas.Netw if networkRange == nil { return fmt.Errorf("network range is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkRange, "", " ") - if err != nil { - return fmt.Errorf("marshal network range: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkRange, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network range: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, networkRange, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(networkRange.NetworkRangeId)) + table.AddRow("ID", utils.PtrString(networkRange.Id)) table.AddSeparator() - table.AddRow("Network range", utils.PtrString(networkRange.Prefix)) + table.AddRow("Network range", networkRange.Prefix) err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/network-area/network-range/describe/describe_test.go b/internal/cmd/network-area/network-range/describe/describe_test.go index a314d11ee..ef75d8fc8 100644 --- a/internal/cmd/network-area/network-range/describe/describe_test.go +++ b/internal/cmd/network-area/network-range/describe/describe_test.go @@ -4,21 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + projectIdFlag = globalflags.ProjectIdFlag + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -36,6 +40,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, } @@ -48,6 +54,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, OrganizationId: utils.Ptr(testOrgId), @@ -61,7 +68,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRangeRequest)) iaas.ApiGetNetworkAreaRangeRequest { - request := testClient.GetNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testNetworkRangeId) + request := testClient.DefaultAPI.GetNetworkAreaRange(testCtx, testOrgId, testNetworkAreaId, testRegion, testNetworkRangeId) for _, mod := range mods { mod(&request) } @@ -156,8 +163,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -189,7 +196,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -227,7 +234,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -259,11 +266,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkRange); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkRange); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/network-range/list/list.go b/internal/cmd/network-area/network-range/list/list.go index e4531121f..72dfdb0e0 100644 --- a/internal/cmd/network-area/network-range/list/list.go +++ b/internal/cmd/network-area/network-range/list/list.go @@ -2,11 +2,12 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -31,11 +31,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel Limit *int64 - OrganizationId *string - NetworkAreaId *string + OrganizationId string + NetworkAreaId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all network ranges in a STACKIT Network Area (SNA)", @@ -55,9 +55,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit network-area network-range list --network-area-id xxx --organization-id yyy --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -75,24 +75,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list network ranges: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - var networkAreaLabel string - networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) - networkAreaLabel = *model.NetworkAreaId - } - params.Printer.Info("No network ranges found for SNA %q\n", networkAreaLabel) - return nil + items := resp.GetItems() + + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) + networkAreaLabel = model.NetworkAreaId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, items) }, } configureFlags(cmd) @@ -108,7 +104,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) if limit != nil && *limit < 1 { @@ -121,53 +117,32 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworkAreaRangesRequest { - return apiClient.ListNetworkAreaRanges(ctx, *model.OrganizationId, *model.NetworkAreaId) + return apiClient.DefaultAPI.ListNetworkAreaRanges(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) } -func outputResult(p *print.Printer, outputFormat string, networkRanges []iaas.NetworkRange) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkRanges, "", " ") - if err != nil { - return fmt.Errorf("marshal network ranges: %w", err) +func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, networkRanges []iaas.NetworkRange) error { + return p.OutputResult(outputFormat, networkRanges, func() error { + if len(networkRanges) == 0 { + p.Outputf("No network ranges found for STACKIT network area %q\n", networkAreaLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkRanges, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network ranges: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "Network Range") for _, networkRange := range networkRanges { - table.AddRow(utils.PtrString(networkRange.NetworkRangeId), utils.PtrString(networkRange.Prefix)) + table.AddRow(utils.PtrString(networkRange.Id), networkRange.Prefix) } p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/network-area/network-range/list/list_test.go b/internal/cmd/network-area/network-range/list/list_test.go index 4e7b52627..26703fc6d 100644 --- a/internal/cmd/network-area/network-range/list/list_test.go +++ b/internal/cmd/network-area/network-range/list/list_test.go @@ -4,26 +4,32 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrganizationId = uuid.NewString() var testNetworkAreaId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrganizationId, networkAreaIdFlag: testNetworkAreaId, limitFlag: "10", @@ -37,10 +43,11 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - OrganizationId: &testOrganizationId, - NetworkAreaId: &testNetworkAreaId, + OrganizationId: testOrganizationId, + NetworkAreaId: testNetworkAreaId, Limit: utils.Ptr(int64(10)), } for _, mod := range mods { @@ -50,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRangesRequest)) iaas.ApiListNetworkAreaRangesRequest { - request := testClient.ListNetworkAreaRanges(testCtx, testOrganizationId, testNetworkAreaId) + request := testClient.DefaultAPI.ListNetworkAreaRanges(testCtx, testOrganizationId, testNetworkAreaId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +67,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRangesRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -140,46 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,7 +172,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -214,8 +183,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - networkRanges []iaas.NetworkRange + outputFormat string + networkAreaLabel string + networkRanges []iaas.NetworkRange } tests := []struct { name string @@ -242,11 +212,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkRanges); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.networkRanges); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/network-range/network_range.go b/internal/cmd/network-area/network-range/network_range.go index 1c52227a8..adf53b654 100644 --- a/internal/cmd/network-area/network-range/network_range.go +++ b/internal/cmd/network-area/network-range/network_range.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/network-range/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/network-range/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/network-range/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "network-range", Aliases: []string{"range"}, @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/network-area/network_area.go b/internal/cmd/network-area/network_area.go index 0b67ea3ea..a47e9585d 100644 --- a/internal/cmd/network-area/network_area.go +++ b/internal/cmd/network-area/network_area.go @@ -6,16 +6,18 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/list" networkrange "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/network-range" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/route" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "network-area", Short: "Provides functionality for STACKIT Network Area (SNA)", @@ -27,12 +29,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(networkrange.NewCmd(params)) + cmd.AddCommand(routingtable.NewCmd(params)) + cmd.AddCommand(region.NewCmd(params)) cmd.AddCommand(route.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) } diff --git a/internal/cmd/network-area/region/create/create.go b/internal/cmd/network-area/region/create/create.go new file mode 100644 index 000000000..185ba10cb --- /dev/null +++ b/internal/cmd/network-area/region/create/create.go @@ -0,0 +1,198 @@ +package create + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + ipv4DefaultNameservers = "ipv4-default-nameservers" + ipv4DefaultPrefixLengthFlag = "ipv4-default-prefix-length" + ipv4MaxPrefixLengthFlag = "ipv4-max-prefix-length" + ipv4MinPrefixLengthFlag = "ipv4-min-prefix-length" + ipv4NetworkRangesFlag = "ipv4-network-ranges" + ipv4TransferNetworkFlag = "ipv4-transfer-network" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string + + IPv4DefaultNameservers []string + IPv4DefaultPrefixLength int64 + IPv4MaxPrefixLength int64 + IPv4MinPrefixLength int64 + IPv4NetworkRanges []string + IPv4TransferNetwork string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a new regional configuration for a STACKIT Network Area (SNA)", + Long: "Creates a new regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24" and ipv4 transfer network "192.168.1.0/24"`, + `$ stackit network-area region create --network-area-id xxx --region eu02 --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24`, + ), + examples.NewExample( + `Create a new regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24`, + ), + examples.NewExample( + `Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + examples.NewExample( + `Create a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region create --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get network area label + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId + } + + prompt := fmt.Sprintf("Are you sure you want to create the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create network area region: %w", err) + } + + if resp == nil || resp.Ipv4 == nil { + return fmt.Errorf("empty response from API") + } + + if !model.Async { + err := spinner.Run(params.Printer, "Create network area region", func() error { + _, err = wait.CreateNetworkAreaRegionWaitHandler(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for network area region creation: %w", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, model.Async, model.Region, networkAreaLabel, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.CIDRSliceFlag(), ipv4NetworkRangesFlag, "Network range to create in CIDR notation") + cmd.Flags().Var(flags.CIDRFlag(), ipv4TransferNetworkFlag, "Transfer network in CIDR notation") + cmd.Flags().StringSlice(ipv4DefaultNameservers, nil, "List of default DNS name server IPs") + cmd.Flags().Int64(ipv4DefaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MaxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MinPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag, ipv4NetworkRangesFlag, ipv4TransferNetworkFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + IPv4DefaultNameservers: flags.FlagToStringSliceValue(p, cmd, ipv4DefaultNameservers), + IPv4DefaultPrefixLength: flags.FlagWithDefaultToInt64Value(p, cmd, ipv4DefaultPrefixLengthFlag), + IPv4MaxPrefixLength: flags.FlagWithDefaultToInt64Value(p, cmd, ipv4MaxPrefixLengthFlag), + IPv4MinPrefixLength: flags.FlagWithDefaultToInt64Value(p, cmd, ipv4MinPrefixLengthFlag), + IPv4NetworkRanges: flags.FlagToStringSliceValue(p, cmd, ipv4NetworkRangesFlag), + IPv4TransferNetwork: flags.FlagToStringValue(p, cmd, ipv4TransferNetworkFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRegionRequest { + req := apiClient.DefaultAPI.CreateNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) + + var networkRange []iaas.NetworkRange + if len(model.IPv4NetworkRanges) > 0 { + networkRange = make([]iaas.NetworkRange, len(model.IPv4NetworkRanges)) + for i := range model.IPv4NetworkRanges { + networkRange[i] = iaas.NetworkRange{ + Prefix: model.IPv4NetworkRanges[i], + } + } + } + + payload := iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: model.IPv4DefaultNameservers, + DefaultPrefixLen: model.IPv4DefaultPrefixLength, + MaxPrefixLen: model.IPv4MaxPrefixLength, + MinPrefixLen: model.IPv4MinPrefixLength, + NetworkRanges: networkRange, + TransferNetwork: model.IPv4TransferNetwork, + }, + } + return req.CreateNetworkAreaRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat string, async bool, region, networkAreaLabel string, regionalArea iaas.RegionalArea) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" + } + p.Outputf("%s region configuration for SNA %q.\nRegion: %s\n", operationState, networkAreaLabel, region) + return nil + }) +} diff --git a/internal/cmd/network-area/region/create/create_test.go b/internal/cmd/network-area/region/create/create_test.go new file mode 100644 index 000000000..c9d9dcd22 --- /dev/null +++ b/internal/cmd/network-area/region/create/create_test.go @@ -0,0 +1,307 @@ +package create + +import ( + "context" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 29 + testMinPrefixLength int64 = 24 + testTransferNetwork = "192.168.2.0/24" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() + testDefaultNameservers = []string{"8.8.8.8", "8.8.4.4"} + testNetworkRanges = []string{"192.168.0.0/24", "10.0.0.0/24"} +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + ipv4DefaultNameservers: strings.Join(testDefaultNameservers, ","), + ipv4DefaultPrefixLengthFlag: strconv.FormatInt(testDefaultPrefixLength, 10), + ipv4MaxPrefixLengthFlag: strconv.FormatInt(testMaxPrefixLength, 10), + ipv4MinPrefixLengthFlag: strconv.FormatInt(testMinPrefixLength, 10), + ipv4NetworkRangesFlag: strings.Join(testNetworkRanges, ","), + ipv4TransferNetworkFlag: testTransferNetwork, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + IPv4DefaultNameservers: testDefaultNameservers, + IPv4DefaultPrefixLength: testDefaultPrefixLength, + IPv4MaxPrefixLength: testMaxPrefixLength, + IPv4MinPrefixLength: testMinPrefixLength, + IPv4NetworkRanges: testNetworkRanges, + IPv4TransferNetwork: testTransferNetwork, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRegionRequest)) iaas.ApiCreateNetworkAreaRegionRequest { + request := testClient.DefaultAPI.CreateNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + request = request.CreateNetworkAreaRegionPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRegionPayload)) iaas.CreateNetworkAreaRegionPayload { + var networkRange []iaas.NetworkRange + if len(testNetworkRanges) > 0 { + networkRange = make([]iaas.NetworkRange, len(testNetworkRanges)) + for i := range testNetworkRanges { + networkRange[i] = iaas.NetworkRange{ + Prefix: testNetworkRanges[i], + } + } + } + + payload := iaas.CreateNetworkAreaRegionPayload{ + Ipv4: &iaas.RegionalAreaIPv4{ + DefaultNameservers: testDefaultNameservers, + DefaultPrefixLen: testDefaultPrefixLength, + MaxPrefixLen: testMaxPrefixLength, + MinPrefixLen: testMinPrefixLength, + NetworkRanges: networkRange, + TransferNetwork: testTransferNetwork, + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "network range missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipv4NetworkRangesFlag) + }), + isValid: false, + }, + { + description: "multiple network ranges", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4NetworkRangesFlag] = "192.168.2.0/24,10.0.0.0/24" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IPv4NetworkRanges = []string{"192.168.2.0/24", "10.0.0.0/24"} + }), + isValid: true, + }, + { + description: "network range invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4NetworkRangesFlag] = "invalid-cidr" + }), + isValid: false, + }, + { + description: "transfer network missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipv4TransferNetworkFlag) + }), + isValid: false, + }, + { + description: "transfer network invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4TransferNetworkFlag] = "" + }), + isValid: false, + }, + { + description: "transfer network invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4TransferNetworkFlag] = "invalid-cidr" + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiCreateNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + async bool + region string + networkAreaLabel string + regionalArea iaas.RegionalArea + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty regional area", + args: args{ + regionalArea: iaas.RegionalArea{}, + }, + wantErr: false, + }, + { + name: "output json", + args: args{ + outputFormat: print.JSONOutputFormat, + regionalArea: iaas.RegionalArea{}, + }, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.region, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/region/delete/delete.go b/internal/cmd/network-area/region/delete/delete.go new file mode 100644 index 000000000..0017e5ff3 --- /dev/null +++ b/internal/cmd/network-area/region/delete/delete.go @@ -0,0 +1,129 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a regional configuration for a STACKIT Network Area (SNA)", + Long: "Deletes a regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + `$ stackit network-area region delete --network-area-id xxx --region eu02 --organization-id yyy`, + ), + examples.NewExample( + `Delete a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region delete --network-area-id xxx --organization-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get network area label + networkAreaName, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaName = model.NetworkAreaId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete network area region: %w", err) + } + + if !model.Async { + err := spinner.Run(params.Printer, "Delete network area region", func() error { + _, err = wait.DeleteNetworkAreaRegionWaitHandler(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region).WaitWithContext(ctx) + return err + }) + if err != nil { + return fmt.Errorf("wait for network area region deletion: %w", err) + } + } + + params.Printer.Outputf("Delete regional network area %q for %q\n", model.Region, networkAreaName) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkAreaRegionRequest { + return apiClient.DefaultAPI.DeleteNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) +} diff --git a/internal/cmd/network-area/region/delete/delete_test.go b/internal/cmd/network-area/region/delete/delete_test.go new file mode 100644 index 000000000..809156ce3 --- /dev/null +++ b/internal/cmd/network-area/region/delete/delete_test.go @@ -0,0 +1,169 @@ +package delete + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRegionRequest)) iaas.ApiDeleteNetworkAreaRegionRequest { + request := testClient.DefaultAPI.DeleteNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiDeleteNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/network-area/region/describe/describe.go b/internal/cmd/network-area/region/describe/describe.go new file mode 100644 index 000000000..ec0e99b6b --- /dev/null +++ b/internal/cmd/network-area/region/describe/describe.go @@ -0,0 +1,161 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describes a regional configuration for a STACKIT Network Area (SNA)", + Long: "Describes a regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + `$ stackit network-area region describe --network-area-id xxx --region eu02 --organization-id yyy`, + ), + examples.NewExample( + `Describe a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region describe --network-area-id xxx --organization-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get network area label + networkAreaName, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + // Set explicit the networkAreaName to empty string and not to the ID, because this is used for the table output + networkAreaName = "" + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("describe network area region: %w", err) + } + + if resp == nil || resp.Ipv4 == nil { + return fmt.Errorf("empty response from API") + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, model.NetworkAreaId, networkAreaName, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkAreaRegionRequest { + return apiClient.DefaultAPI.GetNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) +} + +func outputResult(p *print.Printer, outputFormat, region, areaId, areaName string, regionalArea iaas.RegionalArea) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + table := tables.NewTable() + table.AddRow("ID", areaId) + table.AddSeparator() + if areaName != "" { + table.AddRow("NAME", areaName) + table.AddSeparator() + } + table.AddRow("REGION", region) + table.AddSeparator() + table.AddRow("STATUS", utils.PtrString(regionalArea.Status)) + table.AddSeparator() + if ipv4 := regionalArea.Ipv4; ipv4 != nil { + if ipv4.NetworkRanges != nil { + var networkRanges []string + for _, networkRange := range ipv4.NetworkRanges { + networkRanges = append(networkRanges, networkRange.Prefix) + } + table.AddRow("NETWORK RANGES", strings.Join(networkRanges, ",")) + table.AddSeparator() + } + table.AddRow("TRANSFER RANGE", ipv4.TransferNetwork) + table.AddSeparator() + if len(ipv4.DefaultNameservers) > 0 { + table.AddRow("DNS NAME SERVERS", strings.Join(ipv4.DefaultNameservers, ",")) + table.AddSeparator() + } + table.AddRow("DEFAULT PREFIX LENGTH", ipv4.DefaultPrefixLen) + table.AddSeparator() + table.AddRow("MAX PREFIX LENGTH", ipv4.MaxPrefixLen) + table.AddSeparator() + table.AddRow("MIN PREFIX LENGTH", ipv4.MinPrefixLen) + table.AddSeparator() + } + + if err := table.Display(p); err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/network-area/region/describe/describe_test.go b/internal/cmd/network-area/region/describe/describe_test.go new file mode 100644 index 000000000..34c2d9469 --- /dev/null +++ b/internal/cmd/network-area/region/describe/describe_test.go @@ -0,0 +1,214 @@ +package describe + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRegionRequest)) iaas.ApiGetNetworkAreaRegionRequest { + request := testClient.DefaultAPI.GetNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiGetNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + areaId string + region string + networkAreaLabel string + regionalArea iaas.RegionalArea + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty regional area", + args: args{ + regionalArea: iaas.RegionalArea{}, + }, + wantErr: false, + }, + { + name: "output json", + args: args{ + outputFormat: print.JSONOutputFormat, + regionalArea: iaas.RegionalArea{}, + }, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.region, tt.args.areaId, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/region/list/list.go b/internal/cmd/network-area/region/list/list.go new file mode 100644 index 000000000..0135e86ea --- /dev/null +++ b/internal/cmd/network-area/region/list/list.go @@ -0,0 +1,153 @@ +package list + +import ( + "context" + "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all configured regions for a STACKIT Network Area (SNA)", + Long: "Lists all configured regions for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all configured region for a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + `$ stackit network-area region list --network-area-id xxx --organization-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get network area label + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list network area region: %w", err) + } + + if resp == nil { + return fmt.Errorf("empty response from API") + } + + return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworkAreaRegionsRequest { + return apiClient.DefaultAPI.ListNetworkAreaRegions(ctx, model.OrganizationId, model.NetworkAreaId) +} + +func outputResult(p *print.Printer, outputFormat, areaLabel string, regionalArea iaas.RegionalAreaListResponse) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + if len(regionalArea.Regions) == 0 { + p.Outputf("No regions found for network area %q\n", areaLabel) + return nil + } + + table := tables.NewTable() + table.SetHeader("REGION", "STATUS", "DNS NAME SERVERS", "NETWORK RANGES", "TRANSFER NETWORK") + for region, regionConfig := range regionalArea.Regions { + var dnsNames string + var networkRanges []string + var transferNetwork string + + if ipv4 := regionConfig.Ipv4; ipv4 != nil { + // Set dnsNames + dnsNames = strings.Join(ipv4.DefaultNameservers, ",") + + // Set networkRanges + if len(ipv4.NetworkRanges) > 0 { + for _, networkRange := range ipv4.NetworkRanges { + networkRanges = append(networkRanges, networkRange.Prefix) + } + } + + // Set transferNetwork + transferNetwork = ipv4.TransferNetwork + } + + table.AddRow( + region, + utils.PtrString(regionConfig.Status), + dnsNames, + strings.Join(networkRanges, ","), + transferNetwork, + ) + } + + if err := table.Display(p); err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/network-area/region/list/list_test.go b/internal/cmd/network-area/region/list/list_test.go new file mode 100644 index 000000000..9a480fe56 --- /dev/null +++ b/internal/cmd/network-area/region/list/list_test.go @@ -0,0 +1,219 @@ +package list + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRegionsRequest)) iaas.ApiListNetworkAreaRegionsRequest { + request := testClient.DefaultAPI.ListNetworkAreaRegions(testCtx, testOrgId, testAreaId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiListNetworkAreaRegionsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + networkAreaLabel string + regionalArea iaas.RegionalAreaListResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{}, + }, + wantErr: false, + }, + { + name: "set nil for regions map in response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{ + Regions: nil, + }, + }, + wantErr: false, + }, + { + name: "set empty map for regions map in response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{ + Regions: map[string]iaas.RegionalArea{}, + }, + }, + wantErr: false, + }, + { + name: "set empty region in response", + args: args{ + regionalArea: iaas.RegionalAreaListResponse{ + Regions: map[string]iaas.RegionalArea{ + "eu01": {}, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/region/region.go b/internal/cmd/network-area/region/region.go new file mode 100644 index 000000000..d21eaa106 --- /dev/null +++ b/internal/cmd/network-area/region/region.go @@ -0,0 +1,34 @@ +package region + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/region/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "region", + Short: "Provides functionality for regional configuration of STACKIT Network Area (SNA)", + Long: "Provides functionality for regional configuration of STACKIT Network Area (SNA).", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(create.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/network-area/region/update/update.go b/internal/cmd/network-area/region/update/update.go new file mode 100644 index 000000000..7b1d35add --- /dev/null +++ b/internal/cmd/network-area/region/update/update.go @@ -0,0 +1,165 @@ +package update + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + ipv4DefaultNameservers = "ipv4-default-nameservers" + ipv4DefaultPrefixLengthFlag = "ipv4-default-prefix-length" + ipv4MaxPrefixLengthFlag = "ipv4-max-prefix-length" + ipv4MinPrefixLengthFlag = "ipv4-min-prefix-length" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string + + IPv4DefaultNameservers []string + IPv4DefaultPrefixLength *int64 + IPv4MaxPrefixLength *int64 + IPv4MinPrefixLength *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Updates a existing regional configuration for a STACKIT Network Area (SNA)", + Long: "Updates a existing regional configuration for a STACKIT Network Area (SNA).", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8"`, + `$ stackit network-area region update --network-area-id xxx --region eu02 --organization-id yyy --ipv4-default-nameservers 8.8.8.8`, + ), + examples.NewExample( + `Update a regional configuration "eu02" for a STACKIT Network Area with ID "xxx" in organization with ID "yyy" with new ipv4-default-nameservers "8.8.8.8", using the set region config`, + `$ stackit config set --region eu02`, + `$ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-default-nameservers 8.8.8.8`, + ), + examples.NewExample( + `Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + examples.NewExample( + `Update a new regional configuration for a STACKIT Network Area with ID "xxx" in organization with ID "yyy", ipv4 network range "192.168.0.0/24", ipv4 transfer network "192.168.1.0/24", default prefix length "24", max prefix length "25" and min prefix length "20"`, + `$ stackit network-area region update --network-area-id xxx --organization-id yyy --ipv4-network-ranges 192.168.0.0/24 --ipv4-transfer-network 192.168.1.0/24 --region "eu02" --ipv4-default-prefix-length 24 --ipv4-max-prefix-length 25 --ipv4-min-prefix-length 20`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Get network area label + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId + } + + prompt := fmt.Sprintf("Are you sure you want to update the regional configuration %q for STACKIT Network Area (SNA) %q?", model.Region, networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update network area region: %w", err) + } + + if resp == nil || resp.Ipv4 == nil { + return fmt.Errorf("empty response from API") + } + + return outputResult(params.Printer, model.OutputFormat, model.Region, networkAreaLabel, *resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area (SNA) ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().StringSlice(ipv4DefaultNameservers, nil, "List of default DNS name server IPs") + cmd.Flags().Int64(ipv4DefaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MaxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") + cmd.Flags().Int64(ipv4MinPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") + + // At least one of the flags is required, otherwise there is nothing to update + cmd.MarkFlagsOneRequired(ipv4DefaultNameservers, ipv4MaxPrefixLengthFlag, ipv4MinPrefixLengthFlag, ipv4DefaultPrefixLengthFlag) + + err := flags.MarkFlagsRequired(cmd, networkAreaIdFlag, organizationIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.Region == "" { + return nil, &errors.RegionError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + IPv4DefaultNameservers: flags.FlagToStringSliceValue(p, cmd, ipv4DefaultNameservers), + IPv4DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4DefaultPrefixLengthFlag), + IPv4MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4MaxPrefixLengthFlag), + IPv4MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4MinPrefixLengthFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateNetworkAreaRegionRequest { + req := apiClient.DefaultAPI.UpdateNetworkAreaRegion(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) + + payload := iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: model.IPv4DefaultNameservers, + DefaultPrefixLen: model.IPv4DefaultPrefixLength, + MaxPrefixLen: model.IPv4MaxPrefixLength, + MinPrefixLen: model.IPv4MinPrefixLength, + }, + } + return req.UpdateNetworkAreaRegionPayload(payload) +} + +func outputResult(p *print.Printer, outputFormat, region, networkAreaLabel string, regionalArea iaas.RegionalArea) error { + return p.OutputResult(outputFormat, regionalArea, func() error { + p.Outputf("Updated region configuration for SNA %q.\nRegion: %s\n", networkAreaLabel, region) + return nil + }) +} diff --git a/internal/cmd/network-area/region/update/update_test.go b/internal/cmd/network-area/region/update/update_test.go new file mode 100644 index 000000000..d76c943a3 --- /dev/null +++ b/internal/cmd/network-area/region/update/update_test.go @@ -0,0 +1,265 @@ +package update + +import ( + "context" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testRegion = "eu01" + testDefaultPrefixLength int64 = 25 + testMaxPrefixLength int64 = 29 + testMinPrefixLength int64 = 24 +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +var ( + testAreaId = uuid.NewString() + testOrgId = uuid.NewString() + testDefaultNameservers = []string{"8.8.8.8", "8.8.4.4"} + testNetworkRanges = []string{"192.168.0.0/24", "10.0.0.0/24"} +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + + networkAreaIdFlag: testAreaId, + organizationIdFlag: testOrgId, + ipv4DefaultNameservers: strings.Join(testDefaultNameservers, ","), + ipv4DefaultPrefixLengthFlag: strconv.FormatInt(testDefaultPrefixLength, 10), + ipv4MaxPrefixLengthFlag: strconv.FormatInt(testMaxPrefixLength, 10), + ipv4MinPrefixLengthFlag: strconv.FormatInt(testMinPrefixLength, 10), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrgId, + NetworkAreaId: testAreaId, + IPv4DefaultNameservers: testDefaultNameservers, + IPv4DefaultPrefixLength: utils.Ptr(testDefaultPrefixLength), + IPv4MaxPrefixLength: utils.Ptr(testMaxPrefixLength), + IPv4MinPrefixLength: utils.Ptr(testMinPrefixLength), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdateNetworkAreaRegionRequest)) iaas.ApiUpdateNetworkAreaRegionRequest { + request := testClient.DefaultAPI.UpdateNetworkAreaRegion(testCtx, testOrgId, testAreaId, testRegion) + request = request.UpdateNetworkAreaRegionPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.UpdateNetworkAreaRegionPayload)) iaas.UpdateNetworkAreaRegionPayload { + var networkRange []iaas.NetworkRange + if len(testNetworkRanges) > 0 { + networkRange = make([]iaas.NetworkRange, len(testNetworkRanges)) + for i := range testNetworkRanges { + networkRange[i] = iaas.NetworkRange{ + Prefix: testNetworkRanges[i], + } + } + } + + payload := iaas.UpdateNetworkAreaRegionPayload{ + Ipv4: &iaas.UpdateRegionalAreaIPv4{ + DefaultNameservers: testDefaultNameservers, + DefaultPrefixLen: utils.Ptr(testDefaultPrefixLength), + MaxPrefixLen: utils.Ptr(testMaxPrefixLength), + MinPrefixLen: utils.Ptr(testMinPrefixLength), + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "org id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "org id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "org id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "area id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "area id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "area id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "no update data is set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, ipv4DefaultPrefixLengthFlag) + delete(flagValues, ipv4MaxPrefixLengthFlag) + delete(flagValues, ipv4MinPrefixLengthFlag) + delete(flagValues, ipv4DefaultNameservers) + }), + isValid: false, + }, + { + description: "region empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.RegionFlag] = "" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdateNetworkAreaRegionRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func Test_outputResult(t *testing.T) { + type args struct { + outputFormat string + region string + networkAreaLabel string + regionalArea iaas.RegionalArea + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "set empty regional area", + args: args{ + regionalArea: iaas.RegionalArea{}, + }, + wantErr: false, + }, + { + name: "output json", + args: args{ + outputFormat: print.JSONOutputFormat, + regionalArea: iaas.RegionalArea{}, + }, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.region, tt.args.networkAreaLabel, tt.args.regionalArea); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/route/create/create.go b/internal/cmd/network-area/route/create/create.go index bc0eaf5c3..68974737b 100644 --- a/internal/cmd/network-area/route/create/create.go +++ b/internal/cmd/network-area/route/create/create.go @@ -2,11 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" + "net" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -15,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -23,21 +24,42 @@ import ( const ( organizationIdFlag = "organization-id" networkAreaIdFlag = "network-area-id" - prefixFlag = "prefix" - nexthopFlag = "next-hop" - labelFlag = "labels" + // Deprecated: prefixFlag is deprecated and will be removed after April 2026. Use instead destinationFlag + prefixFlag = "prefix" + destinationFlag = "destination" + // Deprecated: nexthopFlag is deprecated and will be removed after April 2026. Use instead nexthopIPv4Flag or nexthopIPv6Flag + nexthopFlag = "next-hop" + nexthopIPv4Flag = "next-hop-ipv4" + nexthopIPv6Flag = "next-hop-ipv6" + nexthopBlackholeFlag = "nexthop-blackhole" + nexthopInternetFlag = "nexthop-internet" + labelFlag = "labels" +) + +const ( + destinationCIDRv4Type = "cidrv4" + destinationCIDRv6Type = "cidrv6" + + nexthopBlackholeType = "blackhole" + nexthopInternetType = "internet" + nexthopIPv4Type = "ipv4" + nexthopIPv6Type = "ipv6" ) type inputModel struct { *globalflags.GlobalFlagModel - OrganizationId *string - NetworkAreaId *string - Prefix *string - Nexthop *string - Labels *map[string]string + OrganizationId *string + NetworkAreaId *string + DestinationV4 *string + DestinationV6 *string + NexthopV4 *string + NexthopV6 *string + NexthopBlackhole *bool + NexthopInternet *bool + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a static route in a STACKIT Network Area (SNA)", @@ -48,17 +70,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a static route with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, - "$ stackit network-area route create --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1", + `Create a static route with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + "$ stackit network-area route create --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1", ), examples.NewExample( - `Create a static route with labels "key:value" and "foo:bar" with prefix "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, - "$ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --prefix 1.1.1.0/24 --next-hop 1.1.1.1", + `Create a static route with labels "key:value" and "foo:bar" with destination "1.1.1.0/24" and next hop "1.1.1.1" in a STACKIT Network Area with ID "xxx" in organization with ID "yyy"`, + "$ stackit network-area route create --labels key=value,foo=bar --organization-id yyy --network-area-id xxx --destination 1.1.1.0/24 --next-hop 1.1.1.1", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,18 +92,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Get network area label - networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, *model.OrganizationId, *model.NetworkAreaId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) networkAreaLabel = *model.NetworkAreaId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a static route for STACKIT Network Area (SNA) %q?", networkAreaLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a static route for STACKIT Network Area (SNA) %q?", networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -91,16 +111,36 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create static route: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { + if len(resp.Items) == 0 { return fmt.Errorf("empty response from API") } - route, err := iaasUtils.GetRouteFromAPIResponse(*model.Prefix, *model.Nexthop, resp.Items) + var destination string + var nexthop string + if model.DestinationV4 != nil { + destination = *model.DestinationV4 + } else if model.DestinationV6 != nil { + destination = *model.DestinationV6 + } + + if model.NexthopV4 != nil { + nexthop = *model.NexthopV4 + } else if model.NexthopV6 != nil { + nexthop = *model.NexthopV6 + } else if model.NexthopBlackhole != nil { + // For nexthopBlackhole the type is assigned to nexthop, because it doesn't have any value + nexthop = nexthopBlackholeType + } else if model.NexthopInternet != nil { + // For nexthopInternet the type is assigned to nexthop, because it doesn't have any value + nexthop = nexthopInternetType + } + + route, err := iaasUtils.GetRouteFromAPIResponse(destination, nexthop, resp.Items) if err != nil { return err } - return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, route) + return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, &route) }, } configureFlags(cmd) @@ -111,72 +151,153 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "STACKIT Network Area ID") cmd.Flags().Var(flags.CIDRFlag(), prefixFlag, "Static route prefix") - cmd.Flags().String(nexthopFlag, "", "Next hop IP address. Must be a valid IPv4") + cmd.Flags().Var(flags.CIDRFlag(), destinationFlag, "Destination route. Must be a valid IPv4 or IPv6 CIDR") + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") + cmd.Flags().String(nexthopFlag, "", "Next hop IP address. Must be a valid IPv4") + cmd.Flags().String(nexthopIPv4Flag, "", "Next hop IPv4 address") + cmd.Flags().String(nexthopIPv6Flag, "", "Next hop IPv6 address") + cmd.Flags().Bool(nexthopBlackholeFlag, false, "Sets next hop to black hole") + cmd.Flags().Bool(nexthopInternetFlag, false, "Sets next hop to internet") + + cobra.CheckErr(cmd.Flags().MarkDeprecated(nexthopFlag, fmt.Sprintf("The flag %q is deprecated and will be removed after April 2026. Use instead %q to configure a IPv4 next hop.", nexthopFlag, nexthopBlackholeFlag))) + cobra.CheckErr(cmd.Flags().MarkDeprecated(prefixFlag, fmt.Sprintf("The flag %q is deprecated and will be removed after April 2026. Use instead %q to configure a destination.", prefixFlag, destinationFlag))) + + destinationFlags := []string{prefixFlag, destinationFlag} + nexthopFlags := []string{nexthopFlag, nexthopIPv4Flag, nexthopIPv6Flag, nexthopBlackholeFlag, nexthopInternetFlag} + cmd.MarkFlagsMutuallyExclusive(destinationFlags...) + cmd.MarkFlagsMutuallyExclusive(nexthopFlags...) - err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, prefixFlag, nexthopFlag) + cmd.MarkFlagsOneRequired(destinationFlags...) + cmd.MarkFlagsOneRequired(nexthopFlags...) + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseDestination(input string) (destinationV4, destinationV6 *string, err error) { + ip, _, err := net.ParseCIDR(input) + if err != nil { + return nil, nil, fmt.Errorf("parse CIDR: %w", err) + } + if ip.To4() != nil { // CIDR is IPv4 + destinationV4 = utils.Ptr(input) + return destinationV4, nil, nil + } + // CIDR is IPv6 + destinationV6 = utils.Ptr(input) + return nil, destinationV6, nil +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) - model := inputModel{ - GlobalFlagModel: globalFlags, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), - Prefix: flags.FlagToStringPointer(p, cmd, prefixFlag), - Nexthop: flags.FlagToStringPointer(p, cmd, nexthopFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + var destinationV4, destinationV6 *string + if destination := flags.FlagToStringPointer(p, cmd, destinationFlag); destination != nil { + var err error + destinationV4, destinationV6, err = parseDestination(*destination) + if err != nil { + return nil, err + } } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) + if prefix := flags.FlagToStringPointer(p, cmd, prefixFlag); prefix != nil { + var err error + destinationV4, destinationV6, err = parseDestination(*prefix) if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + return nil, err } } + nexthopIPv4 := flags.FlagToStringPointer(p, cmd, nexthopIPv4Flag) + nexthopIPv6 := flags.FlagToStringPointer(p, cmd, nexthopIPv6Flag) + nexthopInternet := flags.FlagToBoolPointer(p, cmd, nexthopInternetFlag) + nexthopBlackhole := flags.FlagToBoolPointer(p, cmd, nexthopBlackholeFlag) + if nexthop := flags.FlagToStringPointer(p, cmd, nexthopFlag); nexthop != nil { + nexthopIPv4 = nexthop + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), + DestinationV4: destinationV4, + DestinationV6: destinationV6, + NexthopV4: nexthopIPv4, + NexthopV6: nexthopIPv6, + NexthopBlackhole: nexthopBlackhole, + NexthopInternet: nexthopInternet, + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), + } + + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkAreaRouteRequest { - req := apiClient.CreateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId) + req := apiClient.DefaultAPI.CreateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) + + var destinationV4 *iaas.DestinationCIDRv4 + var destinationV6 *iaas.DestinationCIDRv6 + if model.DestinationV4 != nil { + destinationV4 = &iaas.DestinationCIDRv4{ + Type: destinationCIDRv4Type, + Value: *model.DestinationV4, + } + } + if model.DestinationV6 != nil { + destinationV6 = &iaas.DestinationCIDRv6{ + Type: destinationCIDRv6Type, + Value: *model.DestinationV6, + } + } + + var nexthopIPv4 *iaas.NexthopIPv4 + var nexthopIPv6 *iaas.NexthopIPv6 + var nexthopBlackhole *iaas.NexthopBlackhole + var nexthopInternet *iaas.NexthopInternet + + if model.NexthopV4 != nil { + nexthopIPv4 = &iaas.NexthopIPv4{ + Type: nexthopIPv4Type, + Value: *model.NexthopV4, + } + } else if model.NexthopV6 != nil { + nexthopIPv6 = &iaas.NexthopIPv6{ + Type: nexthopIPv6Type, + Value: *model.NexthopV6, + } + } else if model.NexthopBlackhole != nil { + nexthopBlackhole = &iaas.NexthopBlackhole{ + Type: nexthopBlackholeType, + } + } else if model.NexthopInternet != nil { + nexthopInternet = &iaas.NexthopInternet{ + Type: nexthopInternetType, + } + } payload := iaas.CreateNetworkAreaRoutePayload{ - Ipv4: &[]iaas.Route{ + Items: []iaas.Route{ { - Prefix: model.Prefix, - Nexthop: model.Nexthop, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: destinationV4, + DestinationCIDRv6: destinationV6, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: nexthopIPv4, + NexthopIPv6: nexthopIPv6, + NexthopBlackhole: nexthopBlackhole, + NexthopInternet: nexthopInternet, + }, + Labels: model.Labels, }, }, } return req.CreateNetworkAreaRoutePayload(payload) } -func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, route iaas.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(route, "", " ") - if err != nil { - return fmt.Errorf("marshal static route: %w", err) - } - p.Outputln(string(details)) - +func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, route *iaas.Route) error { + return p.OutputResult(outputFormat, route, func() error { + p.Outputf("Created static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.Id)) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal static route: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.RouteId)) - return nil - } + }) } diff --git a/internal/cmd/network-area/route/create/create_test.go b/internal/cmd/network-area/route/create/create_test.go index a1ab93772..c854c4a31 100644 --- a/internal/cmd/network-area/route/create/create_test.go +++ b/internal/cmd/network-area/route/create/create_test.go @@ -4,31 +4,39 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" + testDestinationCIDRv4 = "1.1.1.0/24" + testNexthopIPv4 = "1.1.1.1" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, - prefixFlag: "1.1.1.0/24", - nexthopFlag: "1.1.1.1", + destinationFlag: testDestinationCIDRv4, + nexthopIPv4Flag: testNexthopIPv4, } for _, mod := range mods { mod(flagValues) @@ -40,11 +48,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + DestinationV4: utils.Ptr(testDestinationCIDRv4), + NexthopV4: utils.Ptr(testNexthopIPv4), } for _, mod := range mods { mod(model) @@ -53,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRouteRequest)) iaas.ApiCreateNetworkAreaRouteRequest { - request := testClient.CreateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId) + request := testClient.DefaultAPI.CreateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion) request = request.CreateNetworkAreaRoutePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -63,10 +72,20 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkAreaRouteRequest) func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRoutePayload)) iaas.CreateNetworkAreaRoutePayload { payload := iaas.CreateNetworkAreaRoutePayload{ - Ipv4: &[]iaas.Route{ + Items: []iaas.Route{ { - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: destinationCIDRv4Type, + Value: testDestinationCIDRv4, + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: nexthopIPv4Type, + Value: testNexthopIPv4, + }, + }, }, }, } @@ -79,6 +98,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateNetworkAreaRoutePayload)) i func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -94,7 +114,7 @@ func TestParseInput(t *testing.T) { { description: "next hop missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, nexthopFlag) + delete(flagValues, nexthopIPv4Flag) }), isValid: false, }, @@ -146,23 +166,23 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "prefix missing", + description: "destination missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, prefixFlag) + delete(flagValues, destinationFlag) }), isValid: false, }, { - description: "prefix invalid 1", + description: "destinationFlag invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[prefixFlag] = "" + flagValues[destinationFlag] = "" }), isValid: false, }, { - description: "prefix invalid 2", + description: "destinationFlag invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[prefixFlag] = "invalid-prefix" + flagValues[destinationFlag] = "invalid-destinationFlag" }), isValid: false, }, @@ -172,54 +192,32 @@ func TestParseInput(t *testing.T) { flagValues[labelFlag] = "key=value" }), expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = utils.Ptr(map[string]string{"key": "value"}) + model.Labels = map[string]any{"key": "value"} }), isValid: true, }, + { + description: "conflicting destination and prefix set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[prefixFlag] = testDestinationCIDRv4 + }), + isValid: false, + }, + { + description: "conflicting nexthop and nexthop-ipv4 set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nexthopFlag] = testNexthopIPv4 + }), + isValid: false, + }, + { + description: "conflicting nexthop and nexthop-ipv4 set", + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -238,11 +236,11 @@ func TestBuildRequest(t *testing.T) { { description: "optional labels provided", model: fixtureInputModel(func(model *inputModel) { - model.Labels = utils.Ptr(map[string]string{"key": "value"}) + model.Labels = map[string]any{"key": "value"} }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateNetworkAreaRouteRequest) { - *request = (*request).CreateNetworkAreaRoutePayload(fixturePayload(func(payload *iaas.CreateNetworkAreaRoutePayload) { - (*payload.Ipv4)[0].Labels = utils.Ptr(map[string]interface{}{"key": "value"}) + *request = request.CreateNetworkAreaRoutePayload(fixturePayload(func(payload *iaas.CreateNetworkAreaRoutePayload) { + payload.Items[0].Labels = map[string]any{"key": "value"} })) }), }, @@ -254,7 +252,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -287,11 +285,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.route); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkAreaLabel, &tt.args.route); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/route/delete/delete.go b/internal/cmd/network-area/route/delete/delete.go index 114a5d11d..862bccc62 100644 --- a/internal/cmd/network-area/route/delete/delete.go +++ b/internal/cmd/network-area/route/delete/delete.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -32,7 +34,7 @@ type inputModel struct { RouteId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", routeIdArg), Short: "Deletes a static route in a STACKIT Network Area (SNA)", @@ -57,18 +59,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, *model.OrganizationId, *model.NetworkAreaId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) networkAreaLabel = *model.NetworkAreaId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete static route %q on STACKIT Network Area (SNA) %q?", model.RouteId, networkAreaLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete static route %q on STACKIT Network Area (SNA) %q?", model.RouteId, networkAreaLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -105,19 +105,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu RouteId: routeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkAreaRouteRequest { - req := apiClient.DeleteNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId) + req := apiClient.DefaultAPI.DeleteNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.RouteId) return req } diff --git a/internal/cmd/network-area/route/delete/delete_test.go b/internal/cmd/network-area/route/delete/delete_test.go index 0358eba8c..caa136f10 100644 --- a/internal/cmd/network-area/route/delete/delete_test.go +++ b/internal/cmd/network-area/route/delete/delete_test.go @@ -4,21 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -36,6 +39,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, } @@ -49,6 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), @@ -61,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkAreaRouteRequest)) iaas.ApiDeleteNetworkAreaRouteRequest { - request := testClient.DeleteNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId) + request := testClient.DefaultAPI.DeleteNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion, testRouteId) for _, mod := range mods { mod(&request) } @@ -156,8 +162,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -189,7 +195,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -227,7 +233,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/network-area/route/describe/describe.go b/internal/cmd/network-area/route/describe/describe.go index e58a3a2d5..5e8a432ac 100644 --- a/internal/cmd/network-area/route/describe/describe.go +++ b/internal/cmd/network-area/route/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -35,7 +35,7 @@ type inputModel struct { RouteId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", routeIdArg), Short: "Shows details of a static route in a STACKIT Network Area (SNA)", @@ -71,7 +71,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("describe static route: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -97,51 +97,53 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu RouteId: routeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkAreaRouteRequest { - req := apiClient.GetNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId) + req := apiClient.DefaultAPI.GetNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.RouteId) return req } -func outputResult(p *print.Printer, outputFormat string, route iaas.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(route, "", " ") - if err != nil { - return fmt.Errorf("marshal static route: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal static route: %w", err) - } - p.Outputln(string(details)) - - return nil - default: +func outputResult(p *print.Printer, outputFormat string, route *iaas.Route) error { + return p.OutputResult(outputFormat, route, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(route.RouteId)) + table.AddRow("ID", utils.PtrString(route.Id)) table.AddSeparator() - table.AddRow("PREFIX", utils.PtrString(route.Prefix)) - table.AddSeparator() - table.AddRow("NEXTHOP", utils.PtrString(route.Nexthop)) - if route.Labels != nil && len(*route.Labels) > 0 { + destination := route.Destination + if destination.DestinationCIDRv4 != nil { + table.AddRow("DESTINATION TYPE", destination.DestinationCIDRv4.Type) + table.AddSeparator() + table.AddRow("DESTINATION", destination.DestinationCIDRv4.Value) + table.AddSeparator() + } else if destination.DestinationCIDRv6 != nil { + table.AddRow("DESTINATION TYPE", destination.DestinationCIDRv6.Type) + table.AddSeparator() + table.AddRow("DESTINATION", destination.DestinationCIDRv6.Value) + table.AddSeparator() + } + nexthop := route.Nexthop + if nexthop.NexthopIPv4 != nil { + table.AddRow("NEXTHOP", nexthop.NexthopIPv4.Value) + table.AddSeparator() + table.AddRow("NEXTHOP TYPE", nexthop.NexthopIPv4.Type) + table.AddSeparator() + } else if nexthop.NexthopIPv6 != nil { + table.AddRow("NEXTHOP", nexthop.NexthopIPv6.Value) + table.AddSeparator() + table.AddRow("NEXTHOP TYPE", nexthop.NexthopIPv6.Type) + table.AddSeparator() + } else if nexthop.NexthopBlackhole != nil { + table.AddRow("NEXTHOP TYPE", nexthop.NexthopBlackhole.Type) + table.AddSeparator() + } else if nexthop.NexthopInternet != nil { + table.AddRow("NEXTHOP TYPE", nexthop.NexthopInternet.Type) + table.AddSeparator() + } + if len(route.Labels) > 0 { labels := []string{} - for key, value := range *route.Labels { + for key, value := range route.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddSeparator() @@ -153,5 +155,5 @@ func outputResult(p *print.Printer, outputFormat string, route iaas.Route) error return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/network-area/route/describe/describe_test.go b/internal/cmd/network-area/route/describe/describe_test.go index 0c674de81..8e454ad06 100644 --- a/internal/cmd/network-area/route/describe/describe_test.go +++ b/internal/cmd/network-area/route/describe/describe_test.go @@ -4,21 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -36,6 +39,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, } @@ -49,6 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), @@ -61,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkAreaRouteRequest)) iaas.ApiGetNetworkAreaRouteRequest { - request := testClient.GetNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId) + request := testClient.DefaultAPI.GetNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion, testRouteId) for _, mod := range mods { mod(&request) } @@ -156,8 +162,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -189,7 +195,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -227,7 +233,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -259,11 +265,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.route); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, &tt.args.route); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/route/list/list.go b/internal/cmd/network-area/route/list/list.go index 2ea248551..c9ef46f6a 100644 --- a/internal/cmd/network-area/route/list/list.go +++ b/internal/cmd/network-area/route/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -30,11 +30,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel Limit *int64 - OrganizationId *string - NetworkAreaId *string + OrganizationId string + NetworkAreaId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all static routes in a STACKIT Network Area (SNA)", @@ -54,9 +54,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit network-area route list --network-area-id xxx --organization-id yyy --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -74,24 +74,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list static routes: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - var networkAreaLabel string - networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) - networkAreaLabel = *model.NetworkAreaId - } - params.Printer.Info("No static routes found for STACKIT Network Area %q\n", networkAreaLabel) - return nil + items := resp.GetItems() + + var networkAreaLabel string + networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = model.NetworkAreaId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, items) }, } configureFlags(cmd) @@ -107,7 +104,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) if limit != nil && *limit < 1 { @@ -120,57 +117,60 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworkAreaRoutesRequest { - return apiClient.ListNetworkAreaRoutes(ctx, *model.OrganizationId, *model.NetworkAreaId) + return apiClient.DefaultAPI.ListNetworkAreaRoutes(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) } -func outputResult(p *print.Printer, outputFormat string, routes []iaas.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(routes, "", " ") - if err != nil { - return fmt.Errorf("marshal static routes: %w", err) +func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, routes []iaas.Route) error { + return p.OutputResult(outputFormat, routes, func() error { + if len(routes) == 0 { + p.Outputf("No static routes found for STACKIT Network Area %q\n", networkAreaLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(routes, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal static routes: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.SetHeader("Static Route ID", "Next Hop", "Prefix") + table.SetHeader("Static Route ID", "Next Hop", "Next Hop Type", "Destination") for _, route := range routes { + var nextHop string + var nextHopType string + var destination string + routeDest := route.Destination + if routeDest.DestinationCIDRv4 != nil { + destination = routeDest.DestinationCIDRv4.Value + } + if routeDest.DestinationCIDRv6 != nil { + destination = routeDest.DestinationCIDRv6.Value + } + routeNexthop := route.Nexthop + if routeNexthop.NexthopIPv4 != nil { + nextHop = routeNexthop.NexthopIPv4.Value + nextHopType = routeNexthop.NexthopIPv4.Type + } else if routeNexthop.NexthopIPv6 != nil { + nextHop = routeNexthop.NexthopIPv6.Value + nextHopType = routeNexthop.NexthopIPv6.Type + } else if routeNexthop.NexthopBlackhole != nil { + nextHopType = routeNexthop.NexthopBlackhole.Type + } else if routeNexthop.NexthopInternet != nil { + nextHopType = routeNexthop.NexthopInternet.Type + } + table.AddRow( - utils.PtrString(route.RouteId), - utils.PtrString(route.Nexthop), - utils.PtrString(route.Prefix), + utils.PtrString(route.Id), + nextHop, + nextHopType, + destination, ) } p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/network-area/route/list/list_test.go b/internal/cmd/network-area/route/list/list_test.go index 2d66bad9f..b83a47ce0 100644 --- a/internal/cmd/network-area/route/list/list_test.go +++ b/internal/cmd/network-area/route/list/list_test.go @@ -4,26 +4,32 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrganizationId = uuid.NewString() var testNetworkAreaId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrganizationId, networkAreaIdFlag: testNetworkAreaId, limitFlag: "10", @@ -38,9 +44,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - OrganizationId: &testOrganizationId, - NetworkAreaId: &testNetworkAreaId, + OrganizationId: testOrganizationId, + NetworkAreaId: testNetworkAreaId, Limit: utils.Ptr(int64(10)), } for _, mod := range mods { @@ -50,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRoutesRequest)) iaas.ApiListNetworkAreaRoutesRequest { - request := testClient.ListNetworkAreaRoutes(testCtx, testOrganizationId, testNetworkAreaId) + request := testClient.DefaultAPI.ListNetworkAreaRoutes(testCtx, testOrganizationId, testNetworkAreaId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +67,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListNetworkAreaRoutesRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -140,46 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,7 +172,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -214,8 +183,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - routes []iaas.Route + outputFormat string + networkAreaLabel string + routes []iaas.Route } tests := []struct { name string @@ -241,12 +211,29 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "empty destination in route", + args: args{ + routes: []iaas.Route{{ + Destination: iaas.RouteDestination{}, + }}, + }, + wantErr: false, + }, + { + name: "empty nexthop in route", + args: args{ + routes: []iaas.Route{{ + Nexthop: iaas.RouteNexthop{}, + }}, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.routes); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.routes); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/route/routes.go b/internal/cmd/network-area/route/routes.go index 1769f349e..f6d2b3656 100644 --- a/internal/cmd/network-area/route/routes.go +++ b/internal/cmd/network-area/route/routes.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/route/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/route/list" "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/route/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "route", Short: "Provides functionality for static routes in STACKIT Network Areas", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/network-area/route/update/update.go b/internal/cmd/network-area/route/update/update.go index f644da320..5cbad6a69 100644 --- a/internal/cmd/network-area/route/update/update.go +++ b/internal/cmd/network-area/route/update/update.go @@ -2,12 +2,13 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -34,10 +34,10 @@ type inputModel struct { OrganizationId *string NetworkAreaId *string RouteId string - Labels *map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", routeIdArg), Short: "Updates a static route in a STACKIT Network Area (SNA)", @@ -66,7 +66,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Get network area label - networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + networkAreaLabel, err := iaasUtils.GetNetworkAreaName(ctx, apiClient.DefaultAPI, *model.OrganizationId, *model.NetworkAreaId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) networkAreaLabel = *model.NetworkAreaId @@ -79,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create static route: %w", err) } - return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, *resp) + return outputResult(params.Printer, model.OutputFormat, networkAreaLabel, resp) }, } configureFlags(cmd) @@ -99,7 +99,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu routeId := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) - labels := flags.FlagToStringToStringPointer(p, cmd, labelFlag) + labels := flags.FlagToStringToAny(p, cmd, labelFlag) if labels == nil { return nil, &cliErr.EmptyUpdateError{} @@ -113,49 +113,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Labels: labels, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateNetworkAreaRouteRequest { - req := apiClient.UpdateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.RouteId) + req := apiClient.DefaultAPI.UpdateNetworkAreaRoute(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region, model.RouteId) payload := iaas.UpdateNetworkAreaRoutePayload{ - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, } req = req.UpdateNetworkAreaRoutePayload(payload) return req } -func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, route iaas.Route) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(route, "", " ") - if err != nil { - return fmt.Errorf("marshal static route: %w", err) - } - p.Outputln(string(details)) - +func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, route *iaas.Route) error { + return p.OutputResult(outputFormat, route, func() error { + p.Outputf("Updated static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.Id)) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(route, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal static route: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Updated static route for SNA %q.\nStatic route ID: %s\n", networkAreaLabel, utils.PtrString(route.RouteId)) - return nil - } + }) } diff --git a/internal/cmd/network-area/route/update/update_test.go b/internal/cmd/network-area/route/update/update_test.go index 03bdf6da2..edc9cd8cb 100644 --- a/internal/cmd/network-area/route/update/update_test.go +++ b/internal/cmd/network-area/route/update/update_test.go @@ -7,17 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testOrgId = uuid.NewString() var testNetworkAreaId = uuid.NewString() @@ -35,6 +40,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, networkAreaIdFlag: testNetworkAreaId, labelFlag: "value=key", @@ -47,7 +54,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixturePayload(mods ...func(payload *iaas.UpdateNetworkAreaRoutePayload)) iaas.UpdateNetworkAreaRoutePayload { payload := iaas.UpdateNetworkAreaRoutePayload{ - Labels: &map[string]interface{}{ + Labels: map[string]any{ "value": "key", }, } @@ -58,10 +65,10 @@ func fixturePayload(mods ...func(payload *iaas.UpdateNetworkAreaRoutePayload)) i return payload } -func fixturePayloadAsStringMap() map[string]string { +func fixturePayloadAsMap() map[string]any { payload := fixturePayload() - labelsMap := make(map[string]string) - for k, v := range *payload.Labels { + labelsMap := make(map[string]any) + for k, v := range payload.Labels { if value, ok := v.(string); ok { labelsMap[k] = value } @@ -70,15 +77,16 @@ func fixturePayloadAsStringMap() map[string]string { } func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { - payload := fixturePayloadAsStringMap() + payload := fixturePayloadAsMap() model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, OrganizationId: utils.Ptr(testOrgId), NetworkAreaId: utils.Ptr(testNetworkAreaId), RouteId: testRouteId, - Labels: utils.Ptr(payload), + Labels: payload, } for _, mod := range mods { mod(model) @@ -87,7 +95,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateNetworkAreaRouteRequest)) iaas.ApiUpdateNetworkAreaRouteRequest { - request := testClient.UpdateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRouteId) + request := testClient.DefaultAPI.UpdateNetworkAreaRoute(testCtx, testOrgId, testNetworkAreaId, testRegion, testRouteId) request = request.UpdateNetworkAreaRoutePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -189,8 +197,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -222,7 +230,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -260,7 +268,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -293,11 +301,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.route); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.networkAreaLabel, &tt.args.route); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-area/routingtable/create/create.go b/internal/cmd/network-area/routingtable/create/create.go new file mode 100644 index 000000000..e1583e50d --- /dev/null +++ b/internal/cmd/network-area/routingtable/create/create.go @@ -0,0 +1,161 @@ +package create + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + descriptionFlag = "description" + labelFlag = "labels" + nameFlag = "name" + networkAreaIdFlag = "network-area-id" + dynamicRoutesFlag = "dynamic-routes" + systemRoutesFlag = "system-routes" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Description *string + Labels map[string]any + Name string + NetworkAreaId string + SystemRoutes bool + DynamicRoutes bool + OrganizationId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a routing-table", + Long: "Creates a routing-table.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create a routing-table with name "rt"`, + `$ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt"`, + ), + examples.NewExample( + `Create a routing-table with name "rt" and description "some description"`, + `$ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" --description "some description"`, + ), + examples.NewExample( + `Create a routing-table with name "rt" with system routes disabled`, + `$ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" --system-routes=false`, + ), + examples.NewExample( + `Create a routing-table with name "rt" with dynamic routes disabled`, + `$ stackit network-area routing-table create --organization-id xxx --network-area-id yyy --name "rt" --dynamic-routes=false`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + prompt := fmt.Sprintf("Are you sure you want to create the routing-table %q?", model.Name) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + + routingTableResp, err := req.Execute() + if err != nil { + return fmt.Errorf("create routing-table request failed: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, routingTableResp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(descriptionFlag, "", "Description of the routing-table") + cmd.Flags().StringToString(labelFlag, nil, "Key=value labels") + cmd.Flags().String(nameFlag, "", "Name of the routing-table") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Bool(dynamicRoutesFlag, true, "If set to false, prevents dynamic routes from propagating to the routing table.") + cmd.Flags().Bool(systemRoutesFlag, true, "If set to false, disables routes for project-to-project communication.") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, nameFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := &inputModel{ + GlobalFlagModel: globalFlags, + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + DynamicRoutes: flags.FlagToBoolValue(p, cmd, dynamicRoutesFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), + Name: flags.FlagToStringValue(p, cmd, nameFlag), + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + SystemRoutes: flags.FlagToBoolValue(p, cmd, systemRoutesFlag), + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) (iaas.ApiAddRoutingTableToAreaRequest, error) { + payload := iaas.AddRoutingTableToAreaPayload{ + Description: model.Description, + Name: model.Name, + Labels: model.Labels, + SystemRoutes: utils.Ptr(model.SystemRoutes), + DynamicRoutes: utils.Ptr(model.DynamicRoutes), + } + + return apiClient.DefaultAPI.AddRoutingTableToArea( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + ).AddRoutingTableToAreaPayload(payload), nil +} + +func outputResult(p *print.Printer, outputFormat string, routingTable *iaas.RoutingTable) error { + if routingTable == nil { + return fmt.Errorf("routing-table is nil") + } + + if routingTable.Id == nil { + return fmt.Errorf("create routing-table id is empty") + } + + return p.OutputResult(outputFormat, routingTable, func() error { + p.Outputf("Created Routing-Table with ID %q\n", utils.PtrString(routingTable.Id)) + return nil + }) +} diff --git a/internal/cmd/network-area/routingtable/create/create_test.go b/internal/cmd/network-area/routingtable/create/create_test.go new file mode 100644 index 000000000..6aface406 --- /dev/null +++ b/internal/cmd/network-area/routingtable/create/create_test.go @@ -0,0 +1,348 @@ +package create + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() + +const testRoutingTableName = "test" +const testRoutingTableDescription = "test" + +const testSystemRoutesFlag = true +const testDynamicRoutesFlag = true + +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + descriptionFlag: testRoutingTableDescription, + nameFlag: testRoutingTableName, + systemRoutesFlag: strconv.FormatBool(testSystemRoutesFlag), + dynamicRoutesFlag: strconv.FormatBool(testDynamicRoutesFlag), + labelFlag: testLabelSelectorFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + Name: testRoutingTableName, + Description: utils.Ptr(testRoutingTableDescription), + SystemRoutes: testSystemRoutesFlag, + DynamicRoutes: testDynamicRoutesFlag, + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiAddRoutingTableToAreaRequest)) iaas.ApiAddRoutingTableToAreaRequest { + request := testClient.DefaultAPI.AddRoutingTableToArea(testCtx, testOrgId, testNetworkAreaId, testRegion) + request = request.AddRoutingTableToAreaPayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.AddRoutingTableToAreaPayload)) iaas.AddRoutingTableToAreaPayload { + payload := iaas.AddRoutingTableToAreaPayload{ + Description: utils.Ptr(testRoutingTableDescription), + Name: testRoutingTableName, + Labels: testLabels, + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + } + + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "dynamic routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[dynamicRoutesFlag] = "false" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.DynamicRoutes = false + }), + }, + { + description: "system routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[systemRoutesFlag] = "false" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SystemRoutes = false + }), + }, + { + description: "missing organization ID", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid organization ID - empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid organization ID - format", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing network area ID", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "invalid network area ID - empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid network area ID - format", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing name", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nameFlag) + }), + isValid: false, + }, + { + description: "missing labels", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = nil + }), + }, + { + description: "missing description", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, descriptionFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Description = nil + }), + }, + { + description: "no flags provided", + flagValues: map[string]string{}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiAddRoutingTableToAreaRequest + }{ + { + description: "valid input", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "labels missing", + model: fixtureInputModel(func(model *inputModel) { + model.Labels = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutingTableToAreaRequest) { + *request = request.AddRoutingTableToAreaPayload( + fixturePayload(func(payload *iaas.AddRoutingTableToAreaPayload) { + payload.Labels = nil + }), + ) + }), + }, + { + description: "system routes disabled", + model: fixtureInputModel(func(model *inputModel) { + model.SystemRoutes = false + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutingTableToAreaRequest) { + *request = request.AddRoutingTableToAreaPayload( + fixturePayload(func(payload *iaas.AddRoutingTableToAreaPayload) { + payload.SystemRoutes = utils.Ptr(false) + }), + ) + }), + }, + { + description: "dynamic routes disabled", + model: fixtureInputModel(func(model *inputModel) { + model.DynamicRoutes = false + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutingTableToAreaRequest) { + *request = request.AddRoutingTableToAreaPayload( + fixturePayload(func(payload *iaas.AddRoutingTableToAreaPayload) { + payload.DynamicRoutes = utils.Ptr(false) + }), + ) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("buildRequest returned error: %v", err) + } + + if diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{})); diff != "" { + t.Errorf("buildRequest() mismatch (-got +want):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoutingTable := iaas.RoutingTable{ + Id: utils.Ptr("id-foo"), + Name: "route-table-foo", + Description: utils.Ptr("description-foo"), + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + Labels: testLabels, + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable *iaas.RoutingTable + wantErr bool + }{ + { + name: "nil routing-table should return error", + outputFormat: "", + routingTable: nil, + wantErr: true, + }, + { + name: "empty routing-table", + outputFormat: print.PrettyOutputFormat, + routingTable: &iaas.RoutingTable{}, + wantErr: true, + }, + { + name: "pretty output routing-table", + outputFormat: print.PrettyOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "json output routing-table", + outputFormat: print.JSONOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "yaml output routing-table", + outputFormat: print.YAMLOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/delete/delete.go b/internal/cmd/network-area/routingtable/delete/delete.go new file mode 100644 index 000000000..07b7aedc9 --- /dev/null +++ b/internal/cmd/network-area/routingtable/delete/delete.go @@ -0,0 +1,117 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routingTableIdArg = "ROUTING_TABLE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NetworkAreaId string + OrganizationId string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", routingTableIdArg), + Short: "Deletes a routing-table", + Long: "Deletes a routing-table", + Args: args.SingleArg(routingTableIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Delete a routing-table with ID "xxx"`, + `$ stackit network-area routing-table delete xxx --organization-id yyy --network-area-id zzz`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + routingTableLabel, err := iaasUtils.GetRoutingTableOfAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region, model.RoutingTableId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get routing-table name: %v", err) + routingTableLabel = model.RoutingTableId + } else if routingTableLabel == "" { + routingTableLabel = model.RoutingTableId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the routing-table %q?", routingTableLabel) + + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := apiClient.DefaultAPI.DeleteRoutingTableFromArea( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + ) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete routing-table: %w", err) + } + + params.Printer.Outputf("Routing-table %q deleted.", model.RoutingTableId) + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + routingTableId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RoutingTableId: routingTableId, + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/network-area/routingtable/delete/delete_test.go b/internal/cmd/network-area/routingtable/delete/delete_test.go new file mode 100644 index 000000000..0bf08b186 --- /dev/null +++ b/internal/cmd/network-area/routingtable/delete/delete_test.go @@ -0,0 +1,145 @@ +package delete + +import ( + "testing" + + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const testRegion = "eu01" + +var ( + testOrgId = uuid.NewString() + testNetworkAreaId = uuid.NewString() + testRoutingTableId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(func(m *inputModel) { + m.RoutingTableId = testRoutingTableId + }), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing organization ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid organization ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid organization ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing network area ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "invalid network area ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid network area ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing routing-table ID", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - format", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdArg] = "" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} diff --git a/internal/cmd/network-area/routingtable/describe/describe.go b/internal/cmd/network-area/routingtable/describe/describe.go new file mode 100644 index 000000000..a4b81ad87 --- /dev/null +++ b/internal/cmd/network-area/routingtable/describe/describe.go @@ -0,0 +1,152 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routingTableIdArg = "ROUTING_TABLE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NetworkAreaId string + OrganizationId string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", routingTableIdArg), + Short: "Describes a routing-table", + Long: "Describes a routing-table", + Args: args.SingleArg(routingTableIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a routing-table`, + `$ stackit network-area routing-table describe xxx --organization-id xxx --network-area-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := apiClient.DefaultAPI.GetRoutingTableOfArea( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + ) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("describe routing-tables: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, response) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + routingTableId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RoutingTableId: routingTableId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, routingTable *iaas.RoutingTable) error { + if routingTable == nil { + return fmt.Errorf("describe routingtable response is empty") + } + + return p.OutputResult(outputFormat, routingTable, func() error { + table := tables.NewTable() + + table.AddRow("ID", utils.PtrString(routingTable.Id)) + table.AddSeparator() + + table.AddRow("NAME", routingTable.Name) + table.AddSeparator() + + table.AddRow("DESCRIPTION", utils.PtrString(routingTable.Description)) + table.AddSeparator() + + table.AddRow("DEFAULT", utils.PtrString(routingTable.Default)) + table.AddSeparator() + + if len(routingTable.Labels) > 0 { + var labels []string + for key, value := range routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + table.AddRow("LABELS", strings.Join(labels, "\n")) + table.AddSeparator() + } + + table.AddRow("SYSTEM ROUTES", utils.PtrString(routingTable.SystemRoutes)) + table.AddSeparator() + + table.AddRow("DYNAMIC ROUTES", utils.PtrString(routingTable.DynamicRoutes)) + table.AddSeparator() + + table.AddRow("CREATED AT", utils.ConvertTimePToDateTimeString(routingTable.CreatedAt)) + table.AddSeparator() + + table.AddRow("UPDATED AT", utils.ConvertTimePToDateTimeString(routingTable.UpdatedAt)) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/network-area/routingtable/describe/describe_test.go b/internal/cmd/network-area/routingtable/describe/describe_test.go new file mode 100644 index 000000000..44ee066b3 --- /dev/null +++ b/internal/cmd/network-area/routingtable/describe/describe_test.go @@ -0,0 +1,221 @@ +package describe + +import ( + "testing" + "time" + + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRoutingTableId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing organization ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid organization ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid organization ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing network area ID", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "invalid network area ID - empty", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "invalid network area ID - format", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "missing routing-table ID", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "invalid routing-table ID - format", + argValues: []string{"invalid-uuid"}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRouteTable := iaas.RoutingTable{ + CreatedAt: utils.Ptr(time.Now()), + Default: nil, + Description: utils.Ptr("description"), + Id: utils.Ptr("route-foo"), + Labels: testLabels, + Name: "route-foo", + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable *iaas.RoutingTable + wantErr bool + }{ + { + name: "nil routing table", + outputFormat: print.PrettyOutputFormat, + routingTable: nil, + wantErr: true, + }, + { + name: "empty routing table", + outputFormat: print.PrettyOutputFormat, + routingTable: &iaas.RoutingTable{}, + wantErr: false, + }, + { + name: "json empty routing table", + outputFormat: print.JSONOutputFormat, + routingTable: &iaas.RoutingTable{}, + wantErr: false, + }, + { + name: "pretty output one route", + outputFormat: print.PrettyOutputFormat, + routingTable: &dummyRouteTable, + wantErr: false, + }, + { + name: "json output one route", + outputFormat: print.JSONOutputFormat, + routingTable: &dummyRouteTable, + wantErr: false, + }, + { + name: "yaml output one route", + outputFormat: print.YAMLOutputFormat, + routingTable: &dummyRouteTable, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/list/list.go b/internal/cmd/network-area/routingtable/list/list.go new file mode 100644 index 000000000..01c8c8e25 --- /dev/null +++ b/internal/cmd/network-area/routingtable/list/list.go @@ -0,0 +1,176 @@ +package list + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + labelSelectorFlag = "label-selector" + limitFlag = "limit" + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + LabelSelector *string + Limit *int64 + NetworkAreaId string + OrganizationId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all routing-tables", + Long: "Lists all routing-tables", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all routing-tables`, + `$ stackit network-area routing-table list --organization-id xxx --network-area-id yyy`, + ), + examples.NewExample( + `List all routing-tables with labels`, + `$ stackit network-area routing-table list --label-selector env=dev,env=rc --organization-id xxx --network-area-id yyy`, + ), + examples.NewExample( + `List all routing-tables with labels and set limit to 10`, + `$ stackit network-area routing-table list --label-selector env=dev,env=rc --limit 10 --organization-id xxx --network-area-id yyy`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := buildRequest(ctx, model, apiClient) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("list routing-tables: %w", err) + } + + routingTables := response.Items + + // Truncate output + if model.Limit != nil && len(routingTables) > int(*model.Limit) { + routingTables = routingTables[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, routingTables, model.OrganizationId) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + Limit: limit, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListRoutingTablesOfAreaRequest { + request := apiClient.DefaultAPI.ListRoutingTablesOfArea(ctx, model.OrganizationId, model.NetworkAreaId, model.Region) + if model.LabelSelector != nil { + request = request.LabelSelector(*model.LabelSelector) + } + + return request +} + +func outputResult(p *print.Printer, outputFormat string, routingTables []iaas.RoutingTable, orgId string) error { + if routingTables == nil { + return fmt.Errorf("list routing-table items are nil") + } + + return p.OutputResult(outputFormat, routingTables, func() error { + if len(routingTables) == 0 { + p.Outputf("No routing-tables found for organization %q\n", orgId) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "DESCRIPTION", "DEFAULT", "LABELS", "SYSTEM ROUTES", "DYNAMIC ROUTES", "CREATED AT", "UPDATED AT") + for _, routingTable := range routingTables { + var labels []string + if len(routingTable.Labels) > 0 { + for key, value := range routingTable.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + } + + table.AddRow( + utils.PtrString(routingTable.Id), + routingTable.Name, + utils.PtrString(routingTable.Description), + utils.PtrString(routingTable.Default), + strings.Join(labels, "\n"), + utils.PtrString(routingTable.SystemRoutes), + utils.PtrString(routingTable.DynamicRoutes), + utils.ConvertTimePToDateTimeString(routingTable.CreatedAt), + utils.ConvertTimePToDateTimeString(routingTable.UpdatedAt), + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/network-area/routingtable/list/list_test.go b/internal/cmd/network-area/routingtable/list/list_test.go new file mode 100644 index 000000000..2cd1fc5ed --- /dev/null +++ b/internal/cmd/network-area/routingtable/list/list_test.go @@ -0,0 +1,280 @@ +package list + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() + +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} +var testLimitFlag = int64(10) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + labelSelectorFlag: testLabelSelectorFlag, + limitFlag: strconv.Itoa(int(testLimitFlag)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureRequest(mods ...func(request *iaas.ApiListRoutingTablesOfAreaRequest)) iaas.ApiListRoutingTablesOfAreaRequest { + request := testClient.DefaultAPI.ListRoutingTablesOfArea(testCtx, testOrgId, testNetworkAreaId, testRegion) + request = request.LabelSelector(testLabelSelectorFlag) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + LabelSelector: utils.Ptr(testLabelSelectorFlag), + Limit: utils.Ptr(testLimitFlag), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "missing network area ID", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "missing organization ID", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "missing labels", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelSelectorFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.LabelSelector = nil + }), + }, + { + description: "missing limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, limitFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = nil + }), + }, + { + description: "invalid limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "negative limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-10" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRouteTable := iaas.RoutingTable{ + CreatedAt: utils.Ptr(time.Now()), + Default: nil, + Description: utils.Ptr("description"), + Id: utils.Ptr("route-foo"), + Labels: testLabels, + Name: "route-foo", + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable []iaas.RoutingTable + wantErr bool + }{ + { + name: "nil routing table", + outputFormat: print.PrettyOutputFormat, + routingTable: nil, + wantErr: true, + }, + { + name: "pretty empty routing table", + outputFormat: print.PrettyOutputFormat, + routingTable: []iaas.RoutingTable{}, + wantErr: false, + }, + { + name: "json empty routing table", + outputFormat: print.JSONOutputFormat, + routingTable: []iaas.RoutingTable{}, + wantErr: false, + }, + { + name: "yaml empty routing table", + outputFormat: print.YAMLOutputFormat, + routingTable: []iaas.RoutingTable{}, + wantErr: false, + }, + { + name: "pretty empty routing table in slice", + outputFormat: print.PrettyOutputFormat, + routingTable: []iaas.RoutingTable{{}}, + wantErr: false, + }, + { + name: "yaml empty routing table in slice", + outputFormat: print.YAMLOutputFormat, + routingTable: []iaas.RoutingTable{{}}, + wantErr: false, + }, + { + name: "pretty output with one route", + outputFormat: print.PrettyOutputFormat, + routingTable: []iaas.RoutingTable{dummyRouteTable}, + wantErr: false, + }, + { + name: "pretty output with multiple routes", + outputFormat: print.PrettyOutputFormat, + routingTable: []iaas.RoutingTable{dummyRouteTable, dummyRouteTable, dummyRouteTable}, + wantErr: false, + }, + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + routingTable: []iaas.RoutingTable{dummyRouteTable}, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + routingTable: []iaas.RoutingTable{dummyRouteTable}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, tt.routingTable, "dummy-org-id"); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiListRoutingTablesOfAreaRequest + }{ + { + description: "valid input with label selector", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "missing label selector", + model: fixtureInputModel(func(model *inputModel) { + model.LabelSelector = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiListRoutingTablesOfAreaRequest) { + *request = testClient.DefaultAPI.ListRoutingTablesOfArea(testCtx, testOrgId, testNetworkAreaId, testRegion) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + if diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{})); diff != "" { + t.Errorf("buildRequest() mismatch (-got +want):\n%s", diff) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/route/create/create.go b/internal/cmd/network-area/routingtable/route/create/create.go new file mode 100644 index 000000000..c43f94af5 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/create/create.go @@ -0,0 +1,274 @@ +package create + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + destinationValueFlag = "destination-value" + labelFlag = "labels" + networkAreaIdFlag = "network-area-id" + nextHopValueFlag = "nexthop-value" + organizationIdFlag = "organization-id" + routingTableIdFlag = "routing-table-id" + + // Destination Type Constants + destTypeCIDRv4 = "cidrv4" + destTypeCIDRv6 = "cidrv6" + + // NextHop Type Constants + nextHopTypeIPv4 = "ipv4" + nextHopTypeIPv6 = "ipv6" + nextHopTypeInternet = "internet" + nextHopTypeBlackhole = "blackhole" +) + +var ( + destinationTypeFlag = flags.StringEnumFlag( + "destination-type", + []string{destTypeCIDRv4, destTypeCIDRv6}, + "Destination type", + flags.StringEnumIgnoreCase[string](), + ) + nextHopTypeFlag = flags.StringEnumFlag( + "nexthop-type", + []string{nextHopTypeIPv4, nextHopTypeIPv6, nextHopTypeInternet, nextHopTypeBlackhole}, + "Next hop type", + flags.StringEnumIgnoreCase[string](), + ) +) + +type inputModel struct { + *globalflags.GlobalFlagModel + DestinationType string + DestinationValue *string + Labels map[string]any + NetworkAreaId string + NextHopType string + NextHopValue *string + OrganizationId string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Creates a route in a routing-table", + Long: "Creates a route in a routing-table.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample("Create a route with CIDRv4 destination and IPv4 nexthop", + `$ stackit network-area routing-table route create --routing-table-id xxx --organization-id yyy --network-area-id zzz --destination-type cidrv4 --destination-value --nexthop-type ipv4 --nexthop-value `), + + examples.NewExample("Create a route with CIDRv6 destination and IPv6 nexthop", + `$ stackit network-area routing-table route create --routing-table-id xxx --organization-id yyy --network-area-id zzz --destination-type cidrv6 --destination-value --nexthop-type ipv6 --nexthop-value `), + + examples.NewExample("Create a route with CIDRv6 destination and Nexthop Internet", + `$ stackit network-area routing-table route create --routing-table-id xxx --organization-id yyy --network-area-id zzz --destination-type cidrv6 --destination-value --nexthop-type internet`), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + routingTableLabel, err := iaasUtils.GetRoutingTableOfAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region, model.RoutingTableId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get routing-table name: %v", err) + routingTableLabel = model.RoutingTableId + } else if routingTableLabel == "" { + routingTableLabel = model.RoutingTableId + } + + prompt := fmt.Sprintf("Are you sure you want to create a route for routing-table %q?", routingTableLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req, err := buildRequest(ctx, model, apiClient) + if err != nil { + return err + } + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create route request failed: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp.GetItems()) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.CIDRFlag(), destinationValueFlag, "Destination value") + cmd.Flags().String(nextHopValueFlag, "", "NextHop value") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + destinationTypeFlag.Register(cmd.Flags()) + nextHopTypeFlag.Register(cmd.Flags()) + + cmd.Flags().StringToString(labelFlag, nil, "Key=value labels") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag, destinationTypeFlag.Name(), destinationValueFlag, nextHopTypeFlag.Name()) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := &inputModel{ + GlobalFlagModel: globalFlags, + DestinationType: destinationTypeFlag.Get(), + DestinationValue: flags.FlagToStringPointer(p, cmd, destinationValueFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + NextHopType: nextHopTypeFlag.Get(), + NextHopValue: flags.FlagToStringPointer(p, cmd, nextHopValueFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RoutingTableId: flags.FlagToStringValue(p, cmd, routingTableIdFlag), + } + + // Next Hop validation logic + switch strings.ToLower(model.NextHopType) { + case nextHopTypeInternet, nextHopTypeBlackhole: + if model.NextHopValue != nil && *model.NextHopValue != "" { + return nil, errors.New("--nexthop-value is not allowed when --nexthop-type is 'internet' or 'blackhole'") + } + case nextHopTypeIPv4, nextHopTypeIPv6: + if model.NextHopValue == nil || *model.NextHopValue == "" { + return nil, errors.New("--nexthop-value is required when --nexthop-type is 'ipv4' or 'ipv6'") + } + default: + return nil, fmt.Errorf("invalid nexthop-type: %q", model.NextHopType) + } + + p.DebugInputModel(model) + return model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) (iaas.ApiAddRoutesToRoutingTableRequest, error) { + destination := buildDestination(model) + nextHop := buildNextHop(model) + + if destination != nil && nextHop != nil { + payload := iaas.AddRoutesToRoutingTablePayload{ + Items: []iaas.Route{ + { + Destination: *destination, + Nexthop: *nextHop, + Labels: model.Labels, + }, + }, + } + + return apiClient.DefaultAPI.AddRoutesToRoutingTable( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + ).AddRoutesToRoutingTablePayload(payload), nil + } + + return iaas.ApiAddRoutesToRoutingTableRequest{}, fmt.Errorf("invalid input") +} + +func buildDestination(model *inputModel) *iaas.RouteDestination { + if model.DestinationValue == nil { + return nil + } + + destinationType := strings.ToLower(model.DestinationType) + switch destinationType { + case destTypeCIDRv4: + return &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: model.DestinationType, + Value: *model.DestinationValue, + }, + } + case destTypeCIDRv6: + return &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ + Type: model.DestinationType, + Value: *model.DestinationValue, + }, + } + default: + return nil + } +} + +func buildNextHop(model *inputModel) *iaas.RouteNexthop { + nextHopType := strings.ToLower(model.NextHopType) + switch nextHopType { + case nextHopTypeIPv4: + return &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: model.NextHopType, + Value: *model.NextHopValue, + }, + } + case nextHopTypeIPv6: + return &iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ + Type: model.NextHopType, + Value: *model.NextHopValue, + }, + } + case nextHopTypeInternet: + return &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: model.NextHopType, + }, + } + case nextHopTypeBlackhole: + return &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: model.NextHopType, + }, + } + default: + return nil + } +} + +func outputResult(p *print.Printer, outputFormat string, routes []iaas.Route) error { + if len(routes) == 0 { + return fmt.Errorf("create routes response is empty") + } + + return p.OutputResult(outputFormat, routes, func() error { + for _, route := range routes { + p.Outputf("Created route with ID %q\n", utils.PtrString(route.Id)) + } + return nil + }) +} diff --git a/internal/cmd/network-area/routingtable/route/create/create_test.go b/internal/cmd/network-area/routingtable/route/create/create_test.go new file mode 100644 index 000000000..3a4622542 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/create/create_test.go @@ -0,0 +1,701 @@ +package create + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +const testDestinationTypeFlag = destTypeCIDRv4 +const testDestinationValueFlag = "1.1.1.0/24" +const testNextHopTypeFlag = nextHopTypeIPv4 +const testNextHopValueFlag = "1.1.1.1" +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + labelFlag: testLabelSelectorFlag, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + destinationTypeFlag.Name(): testDestinationTypeFlag, + destinationValueFlag: testDestinationValueFlag, + nextHopTypeFlag.Name(): testNextHopTypeFlag, + nextHopValueFlag: testNextHopValueFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + DestinationType: testDestinationTypeFlag, + DestinationValue: utils.Ptr(testDestinationValueFlag), + NextHopType: testNextHopTypeFlag, + NextHopValue: utils.Ptr(testNextHopValueFlag), + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiAddRoutesToRoutingTableRequest)) iaas.ApiAddRoutesToRoutingTableRequest { + request := testClient.DefaultAPI.AddRoutesToRoutingTable(testCtx, testOrgId, testNetworkAreaId, testRegion, testRoutingTableId) + request = request.AddRoutesToRoutingTablePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixturePayload(mods ...func(payload *iaas.AddRoutesToRoutingTablePayload)) iaas.AddRoutesToRoutingTablePayload { + payload := iaas.AddRoutesToRoutingTablePayload{ + Items: []iaas.Route{ + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: testDestinationTypeFlag, + Value: testDestinationValueFlag, + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: testNextHopTypeFlag, + Value: testNextHopValueFlag, + }, + }, + Labels: testLabels, + }, + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "routing-table ID missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "destination value missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, destinationValueFlag) + }), + isValid: false, + }, + { + description: "destination type missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, destinationTypeFlag.Name()) + }), + isValid: false, + }, + { + description: "next hop type missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nextHopTypeFlag.Name()) + }), + isValid: false, + }, + { + description: "next hop value missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, nextHopValueFlag) + }), + isValid: false, + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "organization ID missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "organization ID invalid - empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "" + }), + isValid: false, + }, + { + description: "organization ID invalid - format", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "network area ID missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "network area ID invalid - empty", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "" + }), + isValid: false, + }, + { + description: "network area ID invalid - format", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "invalid destination type enum", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationTypeFlag.Name()] = nextHopTypeIPv4 // Deliberately invalid for dest + }), + isValid: false, + }, + { + description: "destination value not IPv4 CIDR", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationValueFlag] = "0.0.0.0" + }), + isValid: false, + }, + { + description: "destination value not IPv6 CIDR", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationTypeFlag.Name()] = destTypeCIDRv6 + flagValues[destinationValueFlag] = "2001:db8::" + }), + isValid: false, + }, + { + description: "destination value is IPv6 CIDR", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[destinationTypeFlag.Name()] = destTypeCIDRv6 + flagValues[destinationValueFlag] = "2001:db8::/32" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.DestinationType = destTypeCIDRv6 + model.DestinationValue = utils.Ptr("2001:db8::/32") + }), + }, + { + description: "invalid next hop type enum", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = destTypeCIDRv4 // Deliberately invalid for hop + }), + isValid: false, + }, + { + description: "next hop type is internet and next hop value is provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = nextHopTypeInternet + flagValues[nextHopValueFlag] = "1.1.1.1" // should not be allowed + }), + isValid: false, + }, + { + description: "next hop type is blackhole and next hop value is provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = nextHopTypeBlackhole + flagValues[nextHopValueFlag] = "1.1.1.1" + }), + isValid: false, + }, + { + description: "next hop type is internet and next hop value is not provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = nextHopTypeInternet + delete(flagValues, nextHopValueFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NextHopType = nextHopTypeInternet + model.NextHopValue = nil + }), + isValid: true, + }, + { + description: "next hop type is blackhole and next hop value is not provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = nextHopTypeBlackhole + delete(flagValues, nextHopValueFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NextHopType = nextHopTypeBlackhole + model.NextHopValue = nil + }), + isValid: true, + }, + { + description: "next hop type is IPv4 and next hop value is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = nextHopTypeIPv4 + delete(flagValues, nextHopValueFlag) + }), + isValid: false, + }, + { + description: "next hop type is IPv6 and next hop value is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = nextHopTypeIPv6 + delete(flagValues, nextHopValueFlag) + }), + isValid: false, + }, + { + description: "invalid next hop type provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[nextHopTypeFlag.Name()] = "invalid-type" + }), + isValid: false, + }, + { + description: "optional labels are provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[labelFlag] = "key=value" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Labels = map[string]any{"key": "value"} + }), + isValid: true, + }, + { + description: "argument value is empty string", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "argument value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildNextHop(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected *iaas.RouteNexthop + }{ + { + description: "IPv4 next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = nextHopTypeIPv4 + m.NextHopValue = utils.Ptr("1.1.1.1") + }), + expected: &iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: nextHopTypeIPv4, + Value: "1.1.1.1", + }, + }, + }, + { + description: "IPv6 next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = nextHopTypeIPv6 + m.NextHopValue = utils.Ptr("::1") + }), + expected: &iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ + Type: nextHopTypeIPv6, + Value: "::1", + }, + }, + }, + { + description: "Internet next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = nextHopTypeInternet + m.NextHopValue = nil + }), + expected: &iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: nextHopTypeInternet, + }, + }, + }, + { + description: "Blackhole next hop", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = nextHopTypeBlackhole + m.NextHopValue = nil + }), + expected: &iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: nextHopTypeBlackhole, + }, + }, + }, + { + description: "Unsupported next hop type", + model: fixtureInputModel(func(m *inputModel) { + m.NextHopType = "unsupported" + }), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildNextHop(tt.model) + if diff := cmp.Diff(tt.expected, got); diff != "" { + t.Errorf("buildNextHop() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestBuildDestination(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected *iaas.RouteDestination + }{ + { + description: "CIDRv4 destination", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationType = destTypeCIDRv4 + m.DestinationValue = utils.Ptr("192.168.1.0/24") + }), + expected: &iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: destTypeCIDRv4, + Value: "192.168.1.0/24", + }, + }, + }, + { + description: "CIDRv6 destination", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationType = destTypeCIDRv6 + m.DestinationValue = utils.Ptr("2001:db8::/32") + }), + expected: &iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ + Type: destTypeCIDRv6, + Value: "2001:db8::/32", + }, + }, + }, + { + description: "unsupported destination type", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationType = "other" + m.DestinationValue = utils.Ptr("1.1.1.1") + }), + expected: nil, + }, + { + description: "nil destination value", + model: fixtureInputModel(func(m *inputModel) { + m.DestinationValue = nil + }), + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := buildDestination(tt.model) + if diff := cmp.Diff(tt.expected, got); diff != "" { + t.Errorf("buildDestination() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiAddRoutesToRoutingTableRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "optional labels provided", + model: fixtureInputModel(func(model *inputModel) { + model.Labels = map[string]any{"key": "value"} + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + *request = request.AddRoutesToRoutingTablePayload(fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + payload.Items[0].Labels = map[string]any{"key": "value"} + })) + }), + }, + { + description: "destination is cidrv6 and nexthop is ipv6", + model: fixtureInputModel(func(model *inputModel) { + model.DestinationType = destTypeCIDRv6 + model.DestinationValue = utils.Ptr("2001:db8::/32") + model.NextHopType = nextHopTypeIPv6 + model.NextHopValue = utils.Ptr("2001:db8::1") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + *request = request.AddRoutesToRoutingTablePayload(iaas.AddRoutesToRoutingTablePayload{ + Items: []iaas.Route{ + { + Destination: iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ + Type: destTypeCIDRv6, + Value: "2001:db8::/32", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ + Type: nextHopTypeIPv6, + Value: "2001:db8::1", + }, + }, + Labels: testLabels, + }, + }, + }) + }), + }, + { + description: "nexthop type is internet (no value)", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = nextHopTypeInternet + model.NextHopValue = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + payload.Items[0].Nexthop = iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: nextHopTypeInternet, + }, + } + }) + *request = request.AddRoutesToRoutingTablePayload(payload) + }), + }, + { + description: "nexthop type is blackhole (no value)", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = nextHopTypeBlackhole + model.NextHopValue = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + payload.Items[0].Nexthop = iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: nextHopTypeBlackhole, + }, + } + }) + *request = request.AddRoutesToRoutingTablePayload(payload) + }), + }, + { + description: "nexthop type is ipv4 with value", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = nextHopTypeIPv4 + model.NextHopValue = utils.Ptr("1.2.3.4") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + payload.Items[0].Nexthop = iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: nextHopTypeIPv4, + Value: "1.2.3.4", + }, + } + }) + *request = request.AddRoutesToRoutingTablePayload(payload) + }), + }, + { + description: "nexthop type is ipv6 with value", + model: fixtureInputModel(func(model *inputModel) { + model.NextHopType = nextHopTypeIPv6 + model.NextHopValue = utils.Ptr("2001:db8::1") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiAddRoutesToRoutingTableRequest) { + payload := fixturePayload(func(payload *iaas.AddRoutesToRoutingTablePayload) { + payload.Items[0].Nexthop = iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ + Type: nextHopTypeIPv6, + Value: "2001:db8::1", + }, + } + }) + *request = request.AddRoutesToRoutingTablePayload(payload) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildRequest(testCtx, tt.model, testClient) + if err != nil { + t.Fatalf("buildRequest returned error: %v", err) + } + + if diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{})); diff != "" { + t.Errorf("buildRequest() mismatch (-got +want):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaas.Route{ + Id: utils.Ptr("route-foo"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: destTypeCIDRv4, + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: nextHopTypeIPv4, + Value: "10.0.0.1", + }, + }, + Labels: testLabels, + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routes []iaas.Route + wantErr bool + }{ + { + name: "nil routes should return error", + outputFormat: print.PrettyOutputFormat, + routes: nil, + wantErr: true, + }, + { + name: "empty routes list", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{}, + wantErr: true, + }, + { + name: "route list with empty struct", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{{}}, + wantErr: false, + }, + { + name: "pretty output with one route", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{dummyRoute}, + wantErr: false, + }, + { + name: "pretty output with multiple routes", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{dummyRoute, dummyRoute, dummyRoute}, + wantErr: false, + }, + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + routes: []iaas.Route{dummyRoute}, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + routes: []iaas.Route{dummyRoute}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, tt.routes); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/route/delete/delete.go b/internal/cmd/network-area/routingtable/route/delete/delete.go new file mode 100644 index 000000000..b36069e4b --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/delete/delete.go @@ -0,0 +1,121 @@ +package delete + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routeIdArg = "ROUTE_ID" + routingTableIdFlag = "routing-table-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NetworkAreaId string + OrganizationId string + RouteID string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", routingTableIdFlag), + Short: "Deletes a route within a routing-table", + Long: "Deletes a route within a routing-table", + Args: args.SingleArg(routeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Deletes a route within a routing-table`, + `$ stackit network-area routing-table route delete xxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + routingTableLabel, err := iaasUtils.GetRoutingTableOfAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region, model.RoutingTableId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get routing-table name: %v", err) + routingTableLabel = model.RoutingTableId + } else if routingTableLabel == "" { + routingTableLabel = model.RoutingTableId + } + + prompt := fmt.Sprintf("Are you sure you want to delete the route %q in routing-table %q for network area id %q?", model.RouteID, routingTableLabel, model.NetworkAreaId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := apiClient.DefaultAPI.DeleteRouteFromRoutingTable( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + model.RouteID, + ) + err = req.Execute() + if err != nil { + return fmt.Errorf("delete route from routing-table: %w", err) + } + + params.Printer.Outputf("Route %q from routing-table %q deleted.\n", model.RouteID, model.RoutingTableId) + return nil + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + routeId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RouteID: routeId, + RoutingTableId: flags.FlagToStringValue(p, cmd, routingTableIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} diff --git a/internal/cmd/network-area/routingtable/route/delete/delete_test.go b/internal/cmd/network-area/routingtable/route/delete/delete_test.go new file mode 100644 index 000000000..eb956ef98 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/delete/delete_test.go @@ -0,0 +1,133 @@ +package delete + +import ( + "testing" + + "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +var ( + testOrgId = uuid.NewString() + testNetworkAreaId = uuid.NewString() + testRoutingTableId = uuid.NewString() + testRouteId = uuid.NewString() +) + +func fixtureFlagValues(mods ...func(map[string]string)) map[string]string { + flagValues := map[string]string{ + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(*inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.InfoVerbosity, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + RouteID: testRouteId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "valid input", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(func(m *inputModel) { + m.RouteID = testRouteId + }), + }, + { + description: "missing route id arg", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "missing organization-id flag", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "organization-id") + }), + isValid: false, + }, + { + description: "missing network-area-id flag", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "network-area-id") + }), + isValid: false, + }, + { + description: "missing routing-table-id flag", + argValues: []string{testRouteId}, + flagValues: fixtureFlagValues(func(m map[string]string) { + delete(m, "routing-table-id") + }), + isValid: false, + }, + { + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "invalid organization-id flag", + argValues: []string{testRouteId}, + flagValues: map[string]string{"organization-id": "invalid-org"}, + isValid: false, + }, + { + description: "invalid network-area-id flag", + argValues: []string{testRouteId}, + flagValues: map[string]string{"network-area-id": "invalid-area"}, + isValid: false, + }, + { + description: "invalid routing-table-id flag", + argValues: []string{testRouteId}, + flagValues: map[string]string{"routing-table-id": "invalid-table"}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} diff --git a/internal/cmd/network-area/routingtable/route/describe/describe.go b/internal/cmd/network-area/routingtable/route/describe/describe.go new file mode 100644 index 000000000..ddca7c823 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/describe/describe.go @@ -0,0 +1,158 @@ +package describe + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + routeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/network-area/routing-table/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routeIdArg = "ROUTE_ID" + routingTableIdFlag = "routing-table-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NetworkAreaId string + OrganizationId string + RouteID string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", routeIdArg), + Short: "Describes a route within a routing-table", + Long: "Describes a route within a routing-table", + Args: args.SingleArg(routeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Describe a route within a routing-table`, + `$ stackit network-area routing-table route describe xxx --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := apiClient.DefaultAPI.GetRouteOfRoutingTable( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + model.RouteID, + ) + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("describe route: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, response) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + routeId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RouteID: routeId, + RoutingTableId: flags.FlagToStringValue(p, cmd, routingTableIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, route *iaas.Route) error { + if route == nil { + return fmt.Errorf("describe route response is empty") + } + + return p.OutputResult(outputFormat, route, func() error { + routeDetails := routeUtils.ExtractRouteDetails(route) + + table := tables.NewTable() + + table.AddRow("ID", utils.PtrString(route.Id)) + table.AddSeparator() + + table.AddRow("DESTINATION TYPE", routeDetails.DestType) + table.AddSeparator() + + table.AddRow("DESTINATION VALUE", routeDetails.DestValue) + table.AddSeparator() + + table.AddRow("NEXTHOP TYPE", routeDetails.HopType) + table.AddSeparator() + + table.AddRow("NEXTHOP VALUE", routeDetails.HopValue) + table.AddSeparator() + + if len(route.Labels) > 0 { + var labels []string + for key, value := range route.Labels { + labels = append(labels, fmt.Sprintf("%s: %s", key, value)) + } + table.AddRow("LABELS", strings.Join(labels, "\n")) + table.AddSeparator() + } + + table.AddRow("CREATED AT", routeDetails.CreatedAt) + table.AddSeparator() + + table.AddRow("UPDATED AT", routeDetails.UpdatedAt) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/network-area/routingtable/route/describe/describe_test.go b/internal/cmd/network-area/routingtable/route/describe/describe_test.go new file mode 100644 index 000000000..d710a27ca --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/describe/describe_test.go @@ -0,0 +1,227 @@ +package describe + +import ( + "testing" + "time" + + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() +var testRouteId = uuid.NewString() + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + RouteID: testRouteId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRouteId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "invalid routing-table-id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdFlag] = "invalid-id" + }), + isValid: false, + }, + { + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "invalid organization-id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[organizationIdFlag] = "invalid-org" + }), + isValid: false, + }, + { + description: "invalid network-area-id", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[networkAreaIdFlag] = "invalid-area" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaas.Route{ + Id: utils.Ptr("route-foo"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: "cidrv4", + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: "ipv4", + Value: "10.0.0.1", + }, + }, + Labels: testLabels, + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + route *iaas.Route + wantErr bool + }{ + { + name: "nil route should return error", + outputFormat: print.PrettyOutputFormat, + route: nil, + wantErr: true, + }, + { + name: "empty route", + outputFormat: print.PrettyOutputFormat, + route: &iaas.Route{}, + wantErr: false, + }, + { + name: "pretty output with one route", + outputFormat: print.PrettyOutputFormat, + route: &dummyRoute, + wantErr: false, + }, + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + route: &dummyRoute, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + route: &dummyRoute, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, tt.route); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/route/list/list.go b/internal/cmd/network-area/routingtable/route/list/list.go new file mode 100644 index 000000000..bf396ff8a --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/list/list.go @@ -0,0 +1,173 @@ +package list + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + routeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/network-area/routing-table/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + labelSelectorFlag = "label-selector" + limitFlag = "limit" + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routingTableIdFlag = "routing-table-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + LabelSelector *string + Limit *int64 + NetworkAreaId string + OrganizationId string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all routes within a routing-table", + Long: "Lists all routes within a routing-table", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List all routes within a routing-table`, + `$ stackit network-area routing-table route list --routing-table-id xxx --organization-id yyy --network-area-id zzz`, + ), + examples.NewExample( + `List all routes within a routing-table with labels`, + `$ stackit network-area routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc`, + ), + examples.NewExample( + `List all routes within a routing-tables with labels and limit to 10`, + `$ stackit network-area routing-table list --routing-table-id xxx --organization-id yyy --network-area-id zzz --label-selector env=dev,env=rc --limit 10`, + ), + ), + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, nil) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + request := apiClient.DefaultAPI.ListRoutesOfRoutingTable( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + ) + + if model.LabelSelector != nil { + request.LabelSelector(*model.LabelSelector) + } + + response, err := request.Execute() + if err != nil { + return fmt.Errorf("list routes: %w", err) + } + + routes := response.Items + + // Truncate output + if model.Limit != nil && len(routes) > int(*model.Limit) { + routes = routes[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, routes, model.OrganizationId, model.RoutingTableId) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + Limit: limit, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RoutingTableId: flags.FlagToStringValue(p, cmd, routingTableIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, routes []iaas.Route, orgId, routeTableId string) error { + if routes == nil { + return fmt.Errorf("list routes routes are nil") + } + + return p.OutputResult(outputFormat, routes, func() error { + if len(routes) == 0 { + p.Outputf("No routes found for routing-table %q in organization %q\n", routeTableId, orgId) + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "DESTINATION TYPE", "DESTINATION VALUE", "NEXTHOP TYPE", "NEXTHOP VALUE", "LABELS", "CREATED AT", "UPDATED AT") + for _, route := range routes { + routeDetails := routeUtils.ExtractRouteDetails(&route) + table.AddRow( + utils.PtrString(route.Id), + routeDetails.DestType, + routeDetails.DestValue, + routeDetails.HopType, + routeDetails.HopValue, + routeDetails.Labels, + routeDetails.CreatedAt, + routeDetails.UpdatedAt, + ) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/network-area/routingtable/route/list/list_test.go b/internal/cmd/network-area/routingtable/route/list/list_test.go new file mode 100644 index 000000000..f645267d8 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/list/list_test.go @@ -0,0 +1,246 @@ +package list + +import ( + "strconv" + "testing" + "time" + + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +var testLimitFlag = int64(10) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + labelSelectorFlag: testLabelSelectorFlag, + limitFlag: strconv.Itoa(int(testLimitFlag)), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + LabelSelector: utils.Ptr(testLabelSelectorFlag), + Limit: utils.Ptr(testLimitFlag), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "routing-table-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "network-area-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "labels missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelSelectorFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.LabelSelector = nil + }), + }, + { + description: "limit missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, limitFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Limit = nil + }), + }, + { + description: "invalid limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "negative limit flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "-10" + }), + isValid: false, + }, + { + description: "limit zero flag", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaas.Route{ + Id: utils.Ptr("route-foo"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: "cidrv4", + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: "ipv4", + Value: "10.0.0.1", + }, + }, + Labels: testLabels, + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routes []iaas.Route + wantErr bool + }{ + { + name: "nil routes should return error", + outputFormat: print.PrettyOutputFormat, + routes: nil, + wantErr: true, + }, + { + name: "empty routes list", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{}, + wantErr: false, + }, + { + name: "empty routes list json output", + outputFormat: print.JSONOutputFormat, + routes: []iaas.Route{}, + wantErr: false, + }, + { + name: "empty routes list json output", + outputFormat: print.YAMLOutputFormat, + routes: []iaas.Route{}, + wantErr: false, + }, + { + name: "route list with empty struct", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{{}}, + wantErr: false, + }, + { + name: "pretty output with one route", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{dummyRoute}, + wantErr: false, + }, + { + name: "pretty output with multiple routes", + outputFormat: print.PrettyOutputFormat, + routes: []iaas.Route{dummyRoute, dummyRoute, dummyRoute}, + wantErr: false, + }, + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + routes: []iaas.Route{dummyRoute}, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + routes: []iaas.Route{dummyRoute}, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, tt.routes, "dummy-org", "dummy-route-table-id"); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/route/route.go b/internal/cmd/network-area/routingtable/route/route.go new file mode 100644 index 000000000..7b2d05559 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/route.go @@ -0,0 +1,34 @@ +package route + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/route/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/route/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/route/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/route/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/route/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "route", + Short: "Manages routes of a routing-table", + Long: "Manages routes of a routing-table", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(describe.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(delete.NewCmd(params)) + cmd.AddCommand(update.NewCmd(params)) + cmd.AddCommand(create.NewCmd(params)) +} diff --git a/internal/cmd/network-area/routingtable/route/update/update.go b/internal/cmd/network-area/routingtable/route/update/update.go new file mode 100644 index 000000000..11ae98d72 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/update/update.go @@ -0,0 +1,155 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + labelFlag = "labels" + networkAreaIdFlag = "network-area-id" + organizationIdFlag = "organization-id" + routeIdArg = "ROUTE_ID" + routingTableIdFlag = "routing-table-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Labels map[string]any + NetworkAreaId string + OrganizationId string + RouteId string + RoutingTableId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", routeIdArg), + Short: "Updates a route in a routing-table", + Long: "Updates a route in a routing-table.", + Args: args.SingleArg(routeIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Updates the label(s) of a route with ID "xxx" in a routing-table ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit network-area routing-table route update xxx --labels key=value,foo=bar --routing-table-id xxx --organization-id yyy --network-area-id zzz", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + routingTableLabel, err := iaasUtils.GetRoutingTableOfAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region, model.RoutingTableId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get routing-table name: %v", err) + routingTableLabel = model.RoutingTableId + } else if routingTableLabel == "" { + routingTableLabel = model.RoutingTableId + } + + prompt := fmt.Sprintf("Are you sure you want to update route %q for routing-table %q?", model.RouteId, routingTableLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update route %q of routing-table %q : %w", model.RouteId, model.RoutingTableId, err) + } + + return outputResult(params.Printer, model.OutputFormat, model.RoutingTableId, model.NetworkAreaId, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a route. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "Routing-Table ID") + + err := flags.MarkFlagsRequired(cmd, labelFlag, organizationIdFlag, networkAreaIdFlag, routingTableIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + routeId := inputArgs[0] + + labels := flags.FlagToStringToAny(p, cmd, labelFlag) + + if labels == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Labels: labels, + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RouteId: routeId, + RoutingTableId: flags.FlagToStringValue(p, cmd, routingTableIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, routingTableId, networkAreaId string, route *iaas.Route) error { + return p.OutputResult(outputFormat, route, func() error { + if route == nil { + return fmt.Errorf("update route response is empty") + } + + if route.Id == nil || *route.Id == "" { + return fmt.Errorf("update route response has empty id") + } + + p.Outputf("Updated route %q for routing-table %q in network-area %q.\n", *route.Id, routingTableId, networkAreaId) + return nil + }) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateRouteOfRoutingTableRequest { + req := apiClient.DefaultAPI.UpdateRouteOfRoutingTable( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + model.RouteId, + ) + + payload := iaas.UpdateRouteOfRoutingTablePayload{ + Labels: model.Labels, + } + + return req.UpdateRouteOfRoutingTablePayload(payload) +} diff --git a/internal/cmd/network-area/routingtable/route/update/update_test.go b/internal/cmd/network-area/routingtable/route/update/update_test.go new file mode 100644 index 000000000..15c8f4144 --- /dev/null +++ b/internal/cmd/network-area/routingtable/route/update/update_test.go @@ -0,0 +1,301 @@ +package update + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() +var testRouteId = uuid.NewString() + +const testLabelSelectorFlag = "key1=value1,key2=value2" + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + routingTableIdFlag: testRoutingTableId, + labelFlag: testLabelSelectorFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + RoutingTableId: testRoutingTableId, + RouteId: testRouteId, + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRouteId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureRequest(mods ...func(req *iaas.ApiUpdateRouteOfRoutingTableRequest)) iaas.ApiUpdateRouteOfRoutingTableRequest { + req := testClient.DefaultAPI.UpdateRouteOfRoutingTable( + testCtx, + testOrgId, + testNetworkAreaId, + testRegion, + testRoutingTableId, + testRouteId, + ) + + payload := iaas.UpdateRouteOfRoutingTablePayload{ + Labels: testLabels, + } + + req = req.UpdateRouteOfRoutingTablePayload(payload) + + for _, mod := range mods { + mod(&req) + } + + return req +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "routing-table-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: false, + }, + { + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "labels are missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: false, + }, + { + description: "invalid label format", + argValues: []string{}, + flagValues: map[string]string{labelFlag: "invalid-label"}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdateRouteOfRoutingTableRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "labels nil", + model: fixtureInputModel(func(m *inputModel) { + m.Labels = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateRouteOfRoutingTableRequest) { + *request = request.UpdateRouteOfRoutingTablePayload(iaas.UpdateRouteOfRoutingTablePayload{ + Labels: nil, + }) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + gotReq := buildRequest(testCtx, tt.model, testClient) + + if diff := cmp.Diff( + tt.expectedRequest, + gotReq, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ); diff != "" { + t.Errorf("buildRequest() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoute := iaas.Route{ + Id: utils.Ptr("route-foo"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: "cidrv4", + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: "ipv4", + Value: "10.0.0.1", + }, + }, + Labels: testLabels, + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + route *iaas.Route + wantErr bool + }{ + { + name: "nil route should return error", + outputFormat: print.PrettyOutputFormat, + route: nil, + wantErr: true, + }, + { + name: "empty route", + outputFormat: print.PrettyOutputFormat, + route: &iaas.Route{}, + // should fail on pretty format + wantErr: true, + }, + { + name: "pretty output with one route", + outputFormat: print.PrettyOutputFormat, + route: &dummyRoute, + wantErr: false, + }, + { + name: "json output with one route", + outputFormat: print.JSONOutputFormat, + route: &dummyRoute, + wantErr: false, + }, + { + name: "yaml output with one route", + outputFormat: print.YAMLOutputFormat, + route: &dummyRoute, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, "", "", tt.route); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/routingtable/routingtable.go b/internal/cmd/network-area/routingtable/routingtable.go new file mode 100644 index 000000000..514d8146a --- /dev/null +++ b/internal/cmd/network-area/routingtable/routingtable.go @@ -0,0 +1,41 @@ +package routingtable + +import ( + "github.com/spf13/cobra" + + rtCreate "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/create" + rtDelete "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/delete" + rtDescribe "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/describe" + rtList "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/list" + rtRoute "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/route" + rtUpdate "github.com/stackitcloud/stackit-cli/internal/cmd/network-area/routingtable/update" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "routing-table", + Short: "Manage routing-tables and its according routes", + Long: `Manage routing-tables and their associated routes. + +This API is currently available only to selected customers. +To request access, please contact your account manager or submit a support ticket.`, + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand( + rtCreate.NewCmd(params), + rtUpdate.NewCmd(params), + rtList.NewCmd(params), + rtDescribe.NewCmd(params), + rtDelete.NewCmd(params), + rtRoute.NewCmd(params), + ) +} diff --git a/internal/cmd/network-area/routingtable/update/update.go b/internal/cmd/network-area/routingtable/update/update.go new file mode 100644 index 000000000..bdee91c1d --- /dev/null +++ b/internal/cmd/network-area/routingtable/update/update.go @@ -0,0 +1,180 @@ +package update + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + descriptionFlag = "description" + labelFlag = "labels" + nameFlag = "name" + networkAreaIdFlag = "network-area-id" + dynamicRoutesFlag = "dynamic-routes" + systemRoutesFlag = "system-routes" + organizationIdFlag = "organization-id" + routingTableIdArg = "ROUTING_TABLE_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string + NetworkAreaId string + DynamicRoutes *bool + SystemRoutes *bool + RoutingTableId string + Description *string + Labels map[string]any + Name *string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("update %s", routingTableIdArg), + Short: "Updates a routing-table", + Long: "Updates a routing-table.", + Args: args.SingleArg(routingTableIdArg, utils.ValidateUUID), + Example: examples.Build( + examples.NewExample( + `Updates the label(s) of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit network-area routing-table update xxx --labels key=value,foo=bar --organization-id yyy --network-area-id zzz", + ), + examples.NewExample( + `Updates the name of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit network-area routing-table update xxx --name foo --organization-id yyy --network-area-id zzz", + ), + examples.NewExample( + `Updates the description of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit network-area routing-table update xxx --description foo --organization-id yyy --network-area-id zzz", + ), + examples.NewExample( + `Disables the dynamic routes of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit network-area routing-table update xxx --organization-id yyy --network-area-id zzz --dynamic-routes=false", + ), + examples.NewExample( + `Disables the system routes of a routing-table with ID "xxx" in organization with ID "yyy" and network-area with ID "zzz"`, + "$ stackit network-area routing-table update xxx --organization-id yyy --network-area-id zzz --system-routes=false", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + routingTableLabel, err := iaasUtils.GetRoutingTableOfAreaName(ctx, apiClient.DefaultAPI, model.OrganizationId, model.NetworkAreaId, model.Region, model.RoutingTableId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get routing-table name: %v", err) + routingTableLabel = model.RoutingTableId + } else if routingTableLabel == "" { + routingTableLabel = model.RoutingTableId + } + + prompt := fmt.Sprintf("Are you sure you want to update the routing-table %q?", routingTableLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("update routing-table %q : %w", model.RoutingTableId, err) + } + + return outputResult(params.Printer, model.OutputFormat, model.NetworkAreaId, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().String(descriptionFlag, "", "Description of the routing-table") + cmd.Flags().String(nameFlag, "", "Name of the routing-table") + cmd.Flags().StringToString(labelFlag, nil, "Key=value labels") + cmd.Flags().Var(flags.UUIDFlag(), networkAreaIdFlag, "Network-Area ID") + cmd.Flags().Bool(dynamicRoutesFlag, false, "If set to false, prevents dynamic routes from propagating to the routing table.") + cmd.Flags().Bool(systemRoutesFlag, false, "If set to false, disables routes for project-to-project communication.") + cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") + + err := flags.MarkFlagsRequired(cmd, organizationIdFlag, networkAreaIdFlag) + cmd.MarkFlagsOneRequired(dynamicRoutesFlag, systemRoutesFlag, nameFlag, descriptionFlag, labelFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + routeTableId := inputArgs[0] + + model := inputModel{ + GlobalFlagModel: globalFlags, + Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), + Name: flags.FlagToStringPointer(p, cmd, nameFlag), + NetworkAreaId: flags.FlagToStringValue(p, cmd, networkAreaIdFlag), + SystemRoutes: flags.FlagToBoolPointer(p, cmd, systemRoutesFlag), + DynamicRoutes: flags.FlagToBoolPointer(p, cmd, dynamicRoutesFlag), + OrganizationId: flags.FlagToStringValue(p, cmd, organizationIdFlag), + RoutingTableId: routeTableId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat, networkAreaId string, routingTable *iaas.RoutingTable) error { + return p.OutputResult(outputFormat, routingTable, func() error { + if routingTable == nil { + return fmt.Errorf("update routing-table response is empty") + } + + if routingTable.Id == nil { + return fmt.Errorf("update routing-table id is empty") + } + + p.Outputf("Updated routing-table %q in network-area %q.", *routingTable.Id, networkAreaId) + return nil + }) +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateRoutingTableOfAreaRequest { + req := apiClient.DefaultAPI.UpdateRoutingTableOfArea( + ctx, + model.OrganizationId, + model.NetworkAreaId, + model.Region, + model.RoutingTableId, + ) + + payload := iaas.UpdateRoutingTableOfAreaPayload{ + Labels: model.Labels, + Name: model.Name, + Description: model.Description, + DynamicRoutes: model.DynamicRoutes, + SystemRoutes: model.SystemRoutes, + } + + return req.UpdateRoutingTableOfAreaPayload(payload) +} diff --git a/internal/cmd/network-area/routingtable/update/update_test.go b/internal/cmd/network-area/routingtable/update/update_test.go new file mode 100644 index 000000000..f3289a256 --- /dev/null +++ b/internal/cmd/network-area/routingtable/update/update_test.go @@ -0,0 +1,414 @@ +package update + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + +const testRegion = "eu01" + +var testOrgId = uuid.NewString() +var testNetworkAreaId = uuid.NewString() +var testRoutingTableId = uuid.NewString() + +const testRoutingTableName = "test" +const testRoutingTableDescription = "test" +const testLabelSelectorFlag = "key1=value1,key2=value2" + +const testSystemRoutesFlag = true +const testDynamicRoutesFlag = true + +var testLabels = map[string]any{ + "key1": "value1", + "key2": "value2", +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + organizationIdFlag: testOrgId, + networkAreaIdFlag: testNetworkAreaId, + descriptionFlag: testRoutingTableDescription, + nameFlag: testRoutingTableName, + systemRoutesFlag: strconv.FormatBool(testSystemRoutesFlag), + dynamicRoutesFlag: strconv.FormatBool(testDynamicRoutesFlag), + labelFlag: testLabelSelectorFlag, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + OrganizationId: testOrgId, + NetworkAreaId: testNetworkAreaId, + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + SystemRoutes: utils.Ptr(testSystemRoutesFlag), + DynamicRoutes: utils.Ptr(testDynamicRoutesFlag), + Labels: testLabels, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testRoutingTableId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureRequest(mods ...func(request *iaas.ApiUpdateRoutingTableOfAreaRequest)) iaas.ApiUpdateRoutingTableOfAreaRequest { + req := testClient.DefaultAPI.UpdateRoutingTableOfArea( + testCtx, + testOrgId, + testNetworkAreaId, + testRegion, + testRoutingTableId, + ) + + payload := iaas.UpdateRoutingTableOfAreaPayload{ + Labels: testLabels, + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + DynamicRoutes: utils.Ptr(true), + SystemRoutes: utils.Ptr(true), + } + + req = req.UpdateRoutingTableOfAreaPayload(payload) + + for _, mod := range mods { + mod(&req) + } + return req +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + argValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + }), + }, + { + description: "dynamic routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[dynamicRoutesFlag] = "false" + }), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.DynamicRoutes = utils.Ptr(false) + model.RoutingTableId = testRoutingTableId + }), + }, + { + description: "system routes disabled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[systemRoutesFlag] = "false" + }), + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.SystemRoutes = utils.Ptr(false) + model.RoutingTableId = testRoutingTableId + }), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "network-area-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, networkAreaIdFlag) + }), + isValid: false, + }, + { + description: "org-id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, organizationIdFlag) + }), + isValid: false, + }, + { + description: "all required flags missing", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, dynamicRoutesFlag) + delete(flagValues, systemRoutesFlag) + delete(flagValues, nameFlag) + delete(flagValues, labelFlag) + delete(flagValues, descriptionFlag) + }), + isValid: false, + }, + { + description: "all except one required flag missing (description flag)", + argValues: []string{testRoutingTableId}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, dynamicRoutesFlag) + delete(flagValues, systemRoutesFlag) + delete(flagValues, nameFlag) + delete(flagValues, labelFlag) + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + model.DynamicRoutes = nil + model.SystemRoutes = nil + model.Labels = nil + model.Name = nil + model.Description = utils.Ptr(testRoutingTableDescription) + }), + isValid: true, + }, + { + description: "arg value missing", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "arg value wrong", + argValues: []string{"foo-bar"}, + flagValues: fixtureFlagValues(), + isValid: false, + expectedModel: fixtureInputModel(), + }, + { + description: "labels are missing", + argValues: []string{}, + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, labelFlag) + }), + isValid: false, + }, + { + description: "invalid label format", + argValues: []string{}, + flagValues: map[string]string{labelFlag: "invalid-label"}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiUpdateRoutingTableOfAreaRequest + }{ + { + description: "base", + model: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + }), + expectedRequest: fixtureRequest(), + }, + { + description: "labels missing", + model: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + model.Labels = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateRoutingTableOfAreaRequest) { + *request = request.UpdateRoutingTableOfAreaPayload(iaas.UpdateRoutingTableOfAreaPayload{ + Labels: nil, + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + DynamicRoutes: utils.Ptr(true), + SystemRoutes: utils.Ptr(true), + }) + }), + }, + { + description: "name missing", + model: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + model.Name = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateRoutingTableOfAreaRequest) { + *request = request.UpdateRoutingTableOfAreaPayload(iaas.UpdateRoutingTableOfAreaPayload{ + Labels: testLabels, + Name: nil, + Description: utils.Ptr(testRoutingTableDescription), + DynamicRoutes: utils.Ptr(true), + SystemRoutes: utils.Ptr(true), + }) + }), + }, + { + description: "description missing", + model: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + model.Description = nil + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateRoutingTableOfAreaRequest) { + *request = request.UpdateRoutingTableOfAreaPayload(iaas.UpdateRoutingTableOfAreaPayload{ + Labels: testLabels, + Name: utils.Ptr(testRoutingTableName), + Description: nil, + DynamicRoutes: utils.Ptr(true), + SystemRoutes: utils.Ptr(true), + }) + }), + }, + { + description: "dynamic routes disabled", + model: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + model.DynamicRoutes = utils.Ptr(false) + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateRoutingTableOfAreaRequest) { + *request = request.UpdateRoutingTableOfAreaPayload(iaas.UpdateRoutingTableOfAreaPayload{ + Labels: testLabels, + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + DynamicRoutes: utils.Ptr(false), + SystemRoutes: utils.Ptr(true), + }) + }), + }, + { + description: "system routes disabled", + model: fixtureInputModel(func(model *inputModel) { + model.RoutingTableId = testRoutingTableId + model.DynamicRoutes = utils.Ptr(false) + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateRoutingTableOfAreaRequest) { + *request = request.UpdateRoutingTableOfAreaPayload(iaas.UpdateRoutingTableOfAreaPayload{ + Labels: testLabels, + Name: utils.Ptr(testRoutingTableName), + Description: utils.Ptr(testRoutingTableDescription), + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(false), + }) + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + req := buildRequest(testCtx, tt.model, testClient) + + if diff := cmp.Diff(req, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ); diff != "" { + t.Errorf("buildRequest() mismatch (-got +want):\n%s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + dummyRoutingTable := iaas.RoutingTable{ + Id: utils.Ptr("id-foo"), + Name: "route-table-foo", + Description: utils.Ptr("description-foo"), + SystemRoutes: utils.Ptr(true), + DynamicRoutes: utils.Ptr(true), + Labels: testLabels, + CreatedAt: utils.Ptr(time.Now()), + UpdatedAt: utils.Ptr(time.Now()), + } + + tests := []struct { + name string + outputFormat string + routingTable *iaas.RoutingTable + wantErr bool + }{ + { + name: "nil routing-table should return error", + outputFormat: print.PrettyOutputFormat, + routingTable: nil, + wantErr: true, + }, + { + name: "empty routing-table", + outputFormat: print.PrettyOutputFormat, + routingTable: &iaas.RoutingTable{}, + wantErr: true, + }, + { + name: "pretty output routing-table", + outputFormat: print.PrettyOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "json output routing-table", + outputFormat: print.JSONOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + { + name: "yaml output routing-table", + outputFormat: print.YAMLOutputFormat, + routingTable: &dummyRoutingTable, + wantErr: false, + }, + } + + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.outputFormat, "network-area-id", tt.routingTable); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/network-area/update/update.go b/internal/cmd/network-area/update/update.go index 0cc0e2e1e..7db534f50 100644 --- a/internal/cmd/network-area/update/update.go +++ b/internal/cmd/network-area/update/update.go @@ -2,22 +2,21 @@ package update import ( "context" - "encoding/json" "fmt" - rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + rmClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" rmUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -25,29 +24,28 @@ import ( const ( areaIdArg = "AREA_ID" - nameFlag = "name" - organizationIdFlag = "organization-id" - areaIdFlag = "area-id" - dnsNameServersFlag = "dns-name-servers" - defaultPrefixLengthFlag = "default-prefix-length" - maxPrefixLengthFlag = "max-prefix-length" - minPrefixLengthFlag = "min-prefix-length" - labelFlag = "labels" + nameFlag = "name" + organizationIdFlag = "organization-id" + areaIdFlag = "area-id" + labelFlag = "labels" ) +// NetworkAreaResponses is a workaround, to keep the two responses of the iaas v2 api together for the json and yaml output +// Should be removed when the deprecated flags are removed +type NetworkAreaResponses struct { + NetworkArea iaas.NetworkArea `json:"network_area"` + RegionalArea *iaas.RegionalArea `json:"regional_area"` +} + type inputModel struct { *globalflags.GlobalFlagModel - AreaId string - Name *string - OrganizationId *string - DnsNameServers *[]string - DefaultPrefixLength *int64 - MaxPrefixLength *int64 - MinPrefixLength *int64 - Labels *map[string]string + AreaId string + Name *string + OrganizationId *string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", areaIdArg), Short: "Updates a STACKIT Network Area (SNA)", @@ -75,7 +73,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { var orgLabel string rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) if err == nil { - orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) + orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient.DefaultAPI, *model.OrganizationId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) orgLabel = *model.OrganizationId @@ -86,12 +84,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update a network area for organization %q?", orgLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update a network area for organization %q?", orgLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -101,7 +97,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("update network area: %w", err) } - return outputResult(params.Printer, model.OutputFormat, orgLabel, *resp) + if resp == nil || resp.Id == nil { + return fmt.Errorf("update network area: empty response") + } + + responses := NetworkAreaResponses{ + NetworkArea: *resp, + } + + return outputResult(params.Printer, model.OutputFormat, orgLabel, responses) }, } configureFlags(cmd) @@ -111,10 +115,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(nameFlag, "n", "", "Network area name") cmd.Flags().Var(flags.UUIDFlag(), organizationIdFlag, "Organization ID") - cmd.Flags().StringSlice(dnsNameServersFlag, nil, "List of DNS name server IPs") - cmd.Flags().Int64(defaultPrefixLengthFlag, 0, "The default prefix length for networks in the network area") - cmd.Flags().Int64(maxPrefixLengthFlag, 0, "The maximum prefix length for networks in the network area") - cmd.Flags().Int64(minPrefixLengthFlag, 0, "The minimum prefix length for networks in the network area") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network-area. E.g. '--labels key1=value1,key2=value2,...'") err := flags.MarkFlagsRequired(cmd, organizationIdFlag) @@ -127,68 +127,42 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu globalFlags := globalflags.Parse(p, cmd) model := inputModel{ - GlobalFlagModel: globalFlags, - Name: flags.FlagToStringPointer(p, cmd, nameFlag), - OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), - AreaId: areaId, - DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, dnsNameServersFlag), - DefaultPrefixLength: flags.FlagToInt64Pointer(p, cmd, defaultPrefixLengthFlag), - MaxPrefixLength: flags.FlagToInt64Pointer(p, cmd, maxPrefixLengthFlag), - MinPrefixLength: flags.FlagToInt64Pointer(p, cmd, minPrefixLengthFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + GlobalFlagModel: globalFlags, + Name: flags.FlagToStringPointer(p, cmd, nameFlag), + OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), + AreaId: areaId, + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiPartialUpdateNetworkAreaRequest { - req := apiClient.PartialUpdateNetworkArea(ctx, *model.OrganizationId, model.AreaId) + req := apiClient.DefaultAPI.PartialUpdateNetworkArea(ctx, *model.OrganizationId, model.AreaId) payload := iaas.PartialUpdateNetworkAreaPayload{ Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - AddressFamily: &iaas.UpdateAreaAddressFamily{ - Ipv4: &iaas.UpdateAreaIPv4{ - DefaultNameservers: model.DnsNameServers, - DefaultPrefixLen: model.DefaultPrefixLength, - MaxPrefixLen: model.MaxPrefixLength, - MinPrefixLen: model.MinPrefixLength, - }, - }, + Labels: model.Labels, } return req.PartialUpdateNetworkAreaPayload(payload) } -func outputResult(p *print.Printer, outputFormat, projectLabel string, networkArea iaas.NetworkArea) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networkArea, "", " ") - if err != nil { - return fmt.Errorf("marshal network area: %w", err) - } - p.Outputln(string(details)) - +func outputResult(p *print.Printer, outputFormat, projectLabel string, responses NetworkAreaResponses) error { + prettyOutputFunc := func() error { + p.Outputf("Updated STACKIT Network Area for project %q.\n", projectLabel) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networkArea, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network area: %w", err) - } - p.Outputln(string(details)) + } - return nil - default: + // If RegionalArea is NOT set in the reponses, then no deprecated Flags were set. + // In this case, only the response of NetworkArea should be printed in JSON and yaml output, to avoid breaking changes after the deprecated fields are removed + if responses.RegionalArea == nil { + return p.OutputResult(outputFormat, responses.NetworkArea, prettyOutputFunc) + } + + return p.OutputResult(outputFormat, responses, func() error { p.Outputf("Updated STACKIT Network Area for project %q.\n", projectLabel) return nil - } + }) } diff --git a/internal/cmd/network-area/update/update_test.go b/internal/cmd/network-area/update/update_test.go index f25018286..a3a1704fe 100644 --- a/internal/cmd/network-area/update/update_test.go +++ b/internal/cmd/network-area/update/update_test.go @@ -4,24 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" + testName = "example-network-area-name" + testDefaultPrefixLength int64 = 25 + testMinPrefixLength int64 = 24 + testMaxPrefixLength int64 = 26 ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} -var testOrgId = uuid.NewString() -var testAreaId = uuid.NewString() +var ( + testOrgId = uuid.NewString() + testAreaId = uuid.NewString() +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -35,13 +44,11 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - nameFlag: "example-network-area-name", - organizationIdFlag: testOrgId, - dnsNameServersFlag: "1.1.1.0,1.1.2.0", - defaultPrefixLengthFlag: "24", - maxPrefixLengthFlag: "24", - minPrefixLengthFlag: "24", - labelFlag: "key=value", + globalflags.RegionFlag: testRegion, + + nameFlag: testName, + organizationIdFlag: testOrgId, + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -53,17 +60,14 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-area-name"), - OrganizationId: utils.Ptr(testOrgId), - AreaId: testAreaId, - DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - DefaultPrefixLength: utils.Ptr(int64(24)), - MaxPrefixLength: utils.Ptr(int64(24)), - MinPrefixLength: utils.Ptr(int64(24)), - Labels: utils.Ptr(map[string]string{ + Name: utils.Ptr(testName), + OrganizationId: utils.Ptr(testOrgId), + AreaId: testAreaId, + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -72,7 +76,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkAreaRequest)) iaas.ApiPartialUpdateNetworkAreaRequest { - request := testClient.PartialUpdateNetworkArea(testCtx, testOrgId, testAreaId) + request := testClient.DefaultAPI.PartialUpdateNetworkArea(testCtx, testOrgId, testAreaId) request = request.PartialUpdateNetworkAreaPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -82,17 +86,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkAreaReques func fixturePayload(mods ...func(payload *iaas.PartialUpdateNetworkAreaPayload)) iaas.PartialUpdateNetworkAreaPayload { payload := iaas.PartialUpdateNetworkAreaPayload{ - Name: utils.Ptr("example-network-area-name"), - Labels: utils.Ptr(map[string]interface{}{ + Name: utils.Ptr(testName), + Labels: map[string]any{ "key": "value", - }), - AddressFamily: &iaas.UpdateAreaAddressFamily{ - Ipv4: &iaas.UpdateAreaIPv4{ - DefaultNameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - DefaultPrefixLen: utils.Ptr(int64(24)), - MaxPrefixLen: utils.Ptr(int64(24)), - MinPrefixLen: utils.Ptr(int64(24)), - }, }, } for _, mod := range mods { @@ -117,24 +113,6 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(), }, - { - description: "required only", - argValues: fixtureArgValues(), - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, dnsNameServersFlag) - delete(flagValues, defaultPrefixLengthFlag) - delete(flagValues, maxPrefixLengthFlag) - delete(flagValues, minPrefixLengthFlag) - }), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.DnsNameServers = nil - model.DefaultPrefixLength = nil - model.MaxPrefixLength = nil - model.MinPrefixLength = nil - }), - }, - { description: "no values", argValues: []string{}, @@ -206,8 +184,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -239,7 +217,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -277,7 +255,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -290,7 +268,7 @@ func TestOutputResult(t *testing.T) { type args struct { outputFormat string projectLabel string - networkArea iaas.NetworkArea + responses NetworkAreaResponses } tests := []struct { name string @@ -305,16 +283,18 @@ func TestOutputResult(t *testing.T) { { name: "empty network area", args: args{ - networkArea: iaas.NetworkArea{}, + responses: NetworkAreaResponses{ + NetworkArea: iaas.NetworkArea{}, + RegionalArea: nil, + }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.networkArea); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.responses); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-interface/create/create.go b/internal/cmd/network-interface/create/create.go index f96687591..2ca9ce7b7 100644 --- a/internal/cmd/network-interface/create/create.go +++ b/internal/cmd/network-interface/create/create.go @@ -2,13 +2,14 @@ package create import ( "context" - "encoding/json" "fmt" "regexp" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -40,17 +40,17 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - NetworkId *string - AllowedAddresses *[]iaas.AllowedAddressesInner + NetworkId string + AllowedAddresses []iaas.AllowedAddressesInner Ipv4 *string Ipv6 *string - Labels *map[string]string + Labels map[string]any Name *string // <= 63 characters + regex ^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$ NicSecurity *bool - SecurityGroups *[]string // = 36 characters + regex ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + SecurityGroups []string // = 36 characters + regex ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a network interface", @@ -66,9 +66,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit network-interface create --network-id xxx --allowed-addresses "1.1.1.1,8.8.8.8,9.9.9.9" --labels key=value,key2=value2 --name NAME --security-groups "UUID1,UUID2" --nic-security`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -87,12 +87,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a network interface for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a network interface for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -123,7 +121,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -157,11 +155,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } // check security groups size and regex - securityGroups := flags.FlagToStringSlicePointer(p, cmd, securityGroupsFlag) - if securityGroups != nil && len(*securityGroups) > 0 { + securityGroups := flags.FlagToStringSliceValue(p, cmd, securityGroupsFlag) + if len(securityGroups) > 0 { securityGroupsRegex := regexp.MustCompile(securityGroupsRegex) // iterate over them - for _, value := range *securityGroups { + for _, value := range securityGroups { if len(value) != securityGroupLength { return nil, &errors.FlagValidationError{ Flag: securityGroupsFlag, @@ -179,39 +177,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), + NetworkId: flags.FlagToStringValue(p, cmd, networkIdFlag), Ipv4: flags.FlagToStringPointer(p, cmd, ipv4Flag), Ipv6: flags.FlagToStringPointer(p, cmd, ipv6Flag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), Name: name, NicSecurity: flags.FlagToBoolPointer(p, cmd, nicSecurityFlag), SecurityGroups: securityGroups, } if allowedAddresses != nil { - model.AllowedAddresses = utils.Ptr(allowedAddressesInner) - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + model.AllowedAddresses = allowedAddressesInner } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNicRequest { - req := apiClient.CreateNic(ctx, model.ProjectId, *model.NetworkId) + req := apiClient.DefaultAPI.CreateNic(ctx, model.ProjectId, model.Region, model.NetworkId) payload := iaas.CreateNicPayload{ AllowedAddresses: model.AllowedAddresses, Ipv4: model.Ipv4, Ipv6: model.Ipv6, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, Name: model.Name, NicSecurity: model.NicSecurity, SecurityGroups: model.SecurityGroups, @@ -223,25 +213,8 @@ func outputResult(p *print.Printer, outputFormat, projectId string, nic *iaas.NI if nic == nil { return fmt.Errorf("nic is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(nic, "", " ") - if err != nil { - return fmt.Errorf("marshal network interface: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(nic, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network interface: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, nic, func() error { p.Outputf("Created network interface for project %q.\nNIC ID: %s\n", projectId, utils.PtrString(nic.Id)) return nil - } + }) } diff --git a/internal/cmd/network-interface/create/create_test.go b/internal/cmd/network-interface/create/create_test.go index f05ed584e..780317033 100644 --- a/internal/cmd/network-interface/create/create_test.go +++ b/internal/cmd/network-interface/create/create_test.go @@ -7,26 +7,33 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} -var projectIdFlag = globalflags.ProjectIdFlag var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() var testSecurityGroup = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + networkIdFlag: testNetworkId, allowedAddressesFlag: "1.1.1.1,8.8.8.8,9.9.9.9", ipv4Flag: "1.2.3.4", @@ -43,7 +50,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st } func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { - var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{ + var allowedAddresses = []iaas.AllowedAddressesInner{ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")), iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")), iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")), @@ -51,18 +58,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - NetworkId: utils.Ptr(testNetworkId), - AllowedAddresses: utils.Ptr(allowedAddresses), + NetworkId: testNetworkId, + AllowedAddresses: allowedAddresses, Ipv4: utils.Ptr("1.2.3.4"), Ipv6: utils.Ptr("2001:0db8:85a3:08d3::0370:7344"), - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, Name: utils.Ptr("testNic"), NicSecurity: utils.Ptr(true), - SecurityGroups: utils.Ptr([]string{testSecurityGroup}), + SecurityGroups: []string{testSecurityGroup}, } for _, mod := range mods { mod(model) @@ -71,7 +79,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateNicRequest)) iaas.ApiCreateNicRequest { - request := testClient.CreateNic(testCtx, testProjectId, testNetworkId) + request := testClient.DefaultAPI.CreateNic(testCtx, testProjectId, testRegion, testNetworkId) request = request.CreateNicPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -80,21 +88,21 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateNicRequest)) iaas.ApiCre } func fixturePayload(mods ...func(payload *iaas.CreateNicPayload)) iaas.CreateNicPayload { - var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{ + var allowedAddresses = []iaas.AllowedAddressesInner{ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")), iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")), iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")), } payload := iaas.CreateNicPayload{ - AllowedAddresses: utils.Ptr(allowedAddresses), + AllowedAddresses: allowedAddresses, Ipv4: utils.Ptr("1.2.3.4"), Ipv6: utils.Ptr("2001:0db8:85a3:08d3::0370:7344"), - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), + }, Name: utils.Ptr("testNic"), NicSecurity: utils.Ptr(true), - SecurityGroups: utils.Ptr([]string{testSecurityGroup}), + SecurityGroups: []string{testSecurityGroup}, } for _, mod := range mods { mod(&payload) @@ -105,6 +113,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateNicPayload)) iaas.CreateNic func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -190,46 +199,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -253,7 +223,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -286,11 +256,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectId, tt.args.nic); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectId, tt.args.nic); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-interface/delete/delete.go b/internal/cmd/network-interface/delete/delete.go index 8450f5abc..4cdca444c 100644 --- a/internal/cmd/network-interface/delete/delete.go +++ b/internal/cmd/network-interface/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -24,11 +26,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - NetworkId *string + NetworkId string NicId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", nicIdArg), Short: "Deletes a network interface", @@ -53,12 +55,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the network interface %q? (This cannot be undone)", model.NicId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the network interface %q? (This cannot be undone)", model.NicId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -90,23 +90,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), + NetworkId: flags.FlagToStringValue(p, cmd, networkIdFlag), NicId: nicId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNicRequest { - req := apiClient.DeleteNic(ctx, model.ProjectId, *model.NetworkId, model.NicId) + req := apiClient.DefaultAPI.DeleteNic(ctx, model.ProjectId, model.Region, model.NetworkId, model.NicId) return req } diff --git a/internal/cmd/network-interface/delete/delete_test.go b/internal/cmd/network-interface/delete/delete_test.go index ac03d45f6..277de1c13 100644 --- a/internal/cmd/network-interface/delete/delete_test.go +++ b/internal/cmd/network-interface/delete/delete_test.go @@ -7,19 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} -var projectIdFlag = globalflags.ProjectIdFlag var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() var testNicId = uuid.NewString() @@ -36,8 +39,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - networkIdFlag: testNetworkId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + networkIdFlag: testNetworkId, } for _, mod := range mods { mod(flagValues) @@ -50,8 +54,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - NetworkId: utils.Ptr(testNetworkId), + NetworkId: testNetworkId, NicId: testNicId, } for _, mod := range mods { @@ -61,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteNicRequest)) iaas.ApiDeleteNicRequest { - request := testClient.DeleteNic(testCtx, testProjectId, testNetworkId, testNicId) + request := testClient.DefaultAPI.DeleteNic(testCtx, testProjectId, testRegion, testNetworkId, testNicId) for _, mod := range mods { mod(&request) } @@ -122,8 +127,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -155,7 +160,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -193,7 +198,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/network-interface/describe/describe.go b/internal/cmd/network-interface/describe/describe.go index b221a0b1b..27c1f1ff9 100644 --- a/internal/cmd/network-interface/describe/describe.go +++ b/internal/cmd/network-interface/describe/describe.go @@ -2,13 +2,14 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -29,11 +29,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - NetworkId *string + NetworkId string NicId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", nicIdArg), Short: "Describes a network interface", @@ -96,24 +96,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), + NetworkId: flags.FlagToStringValue(p, cmd, networkIdFlag), NicId: nicId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNicRequest { - req := apiClient.GetNic(ctx, model.ProjectId, *model.NetworkId, model.NicId) + req := apiClient.DefaultAPI.GetNic(ctx, model.ProjectId, model.Region, model.NetworkId, model.NicId) return req } @@ -121,24 +113,7 @@ func outputResult(p *print.Printer, outputFormat string, nic *iaas.NIC) error { if nic == nil { return fmt.Errorf("nic is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(nic, "", " ") - if err != nil { - return fmt.Errorf("marshal network interface: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(nic, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network interface: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, nic, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(nic.Id)) table.AddSeparator() @@ -159,17 +134,17 @@ func outputResult(p *print.Printer, outputFormat string, nic *iaas.NIC) error { table.AddRow("MAC", utils.PtrString(nic.Mac)) table.AddSeparator() table.AddRow("NIC SECURITY", utils.PtrString(nic.NicSecurity)) - if nic.AllowedAddresses != nil && len(*nic.AllowedAddresses) > 0 { + if len(nic.AllowedAddresses) > 0 { var allowedAddresses []string - for _, value := range *nic.AllowedAddresses { + for _, value := range nic.AllowedAddresses { allowedAddresses = append(allowedAddresses, *value.String) } table.AddSeparator() table.AddRow("ALLOWED ADDRESSES", strings.Join(allowedAddresses, "\n")) } - if nic.Labels != nil && len(*nic.Labels) > 0 { + if len(nic.Labels) > 0 { var labels []string - for key, value := range *nic.Labels { + for key, value := range nic.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddSeparator() @@ -179,9 +154,9 @@ func outputResult(p *print.Printer, outputFormat string, nic *iaas.NIC) error { table.AddRow("STATUS", utils.PtrString(nic.Status)) table.AddSeparator() table.AddRow("TYPE", utils.PtrString(nic.Type)) - if nic.SecurityGroups != nil && len(*nic.SecurityGroups) > 0 { + if len(nic.SecurityGroups) > 0 { table.AddSeparator() - table.AddRow("SECURITY GROUPS", strings.Join(*nic.SecurityGroups, "\n")) + table.AddRow("SECURITY GROUPS", strings.Join(nic.SecurityGroups, "\n")) } err := table.Display(p) @@ -189,5 +164,5 @@ func outputResult(p *print.Printer, outputFormat string, nic *iaas.NIC) error { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/network-interface/describe/describe_test.go b/internal/cmd/network-interface/describe/describe_test.go index 24163ac87..86570f780 100644 --- a/internal/cmd/network-interface/describe/describe_test.go +++ b/internal/cmd/network-interface/describe/describe_test.go @@ -7,19 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} -var projectIdFlag = globalflags.ProjectIdFlag var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() var testNicId = uuid.NewString() @@ -36,8 +39,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - networkIdFlag: testNetworkId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + networkIdFlag: testNetworkId, } for _, mod := range mods { mod(flagValues) @@ -50,8 +54,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - NetworkId: utils.Ptr(testNetworkId), + NetworkId: testNetworkId, NicId: testNicId, } for _, mod := range mods { @@ -61,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetNicRequest)) iaas.ApiGetNicRequest { - request := testClient.GetNic(testCtx, testProjectId, testNetworkId, testNicId) + request := testClient.DefaultAPI.GetNic(testCtx, testProjectId, testRegion, testNetworkId, testNicId) for _, mod := range mods { mod(&request) } @@ -122,8 +127,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -155,7 +160,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -193,7 +198,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -225,11 +230,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.nic); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.nic); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-interface/list/list.go b/internal/cmd/network-interface/list/list.go index eeb3c9be6..fc5a20ae3 100644 --- a/internal/cmd/network-interface/list/list.go +++ b/internal/cmd/network-interface/list/list.go @@ -1,13 +1,17 @@ package list import ( + "cmp" "context" - "encoding/json" "fmt" + "slices" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +22,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -34,13 +37,18 @@ type inputModel struct { NetworkId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all network interfaces of a network", Long: "Lists all network interfaces of a network.", Args: args.NoArgs, Example: examples.Build( + // Note: this subcommand uses two different API enpoints, which makes the implementation somewhat messy + examples.NewExample( + `Lists all network interfaces`, + `$ stackit network-interface list`, + ), examples.NewExample( `Lists all network interfaces with network ID "xxx"`, `$ stackit network-interface list --network-id xxx`, @@ -58,9 +66,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit network-interface list --network-id xxx --limit 10`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -71,32 +79,52 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - // Call API - req := buildRequest(ctx, model, apiClient) + if model.NetworkId == nil { + // Call API to get all NICs in the Project + req := buildProjectRequest(ctx, model, apiClient) + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list network interfaces: %w", err) + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + projectLabel = model.ProjectId + } + + // Truncate output + items := resp.Items + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputProjectResult(params.Printer, model.OutputFormat, items, projectLabel) + } + + // Call API to get NICs for one Network + req := buildNetworkRequest(ctx, model, apiClient) + resp, err := req.Execute() if err != nil { return fmt.Errorf("list network interfaces: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, *model.NetworkId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) - networkLabel = *model.NetworkId - } else if networkLabel == "" { - networkLabel = *model.NetworkId - } - params.Printer.Info("No network interfaces found for network %q\n", networkLabel) - return nil + networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.NetworkId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) + networkLabel = *model.NetworkId + } else if networkLabel == "" { + networkLabel = *model.NetworkId } // Truncate output - items := *resp.Items + items := resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputNetworkResult(params.Printer, model.OutputFormat, items, networkLabel) }, } configureFlags(cmd) @@ -107,12 +135,9 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID") cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") cmd.Flags().String(labelSelectorFlag, "", "Filter by label") - - err := flags.MarkFlagsRequired(cmd, networkIdFlag) - cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -133,20 +158,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + p.DebugInputModel(model) + return &model, nil +} + +func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListProjectNICsRequest { + req := apiClient.DefaultAPI.ListProjectNICs(ctx, model.ProjectId, model.Region) + if model.LabelSelector != nil { + req = req.LabelSelector(*model.LabelSelector) } - return &model, nil + return req } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest { - req := apiClient.ListNics(ctx, model.ProjectId, *model.NetworkId) +func buildNetworkRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest { + req := apiClient.DefaultAPI.ListNics(ctx, model.ProjectId, model.Region, *model.NetworkId) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } @@ -154,25 +180,46 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, nics []iaas.NIC) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(nics, "", " ") - if err != nil { - return fmt.Errorf("marshal nics: %w", err) +func outputProjectResult(p *print.Printer, outputFormat string, nics []iaas.NIC, projectLabel string) error { + return p.OutputResult(outputFormat, nics, func() error { + if len(nics) == 0 { + p.Outputf("No network interfaces found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(nics, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal nics: %w", err) + slices.SortFunc(nics, func(a, b iaas.NIC) int { + return cmp.Compare(utils.PtrValue(a.NetworkId), utils.PtrValue(b.NetworkId)) + }) + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "NETWORK ID", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE") + + for _, nic := range nics { + table.AddRow( + utils.PtrString(nic.Id), + utils.PtrString(nic.Name), + utils.PtrString(nic.NetworkId), + utils.PtrString(nic.NicSecurity), + utils.PtrString(nic.Device), + utils.PtrString(nic.Ipv4), + utils.PtrString(nic.Status), + utils.PtrString(nic.Type), + ) + table.AddSeparator() } - p.Outputln(string(details)) + p.Outputln(table.Render()) return nil - default: + }) +} + +func outputNetworkResult(p *print.Printer, outputFormat string, nics []iaas.NIC, networkLabel string) error { + return p.OutputResult(outputFormat, nics, func() error { + if len(nics) == 0 { + p.Outputf("No network interfaces found for network %q\n", networkLabel) + return nil + } + table := tables.NewTable() table.SetHeader("ID", "NAME", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE") @@ -191,5 +238,5 @@ func outputResult(p *print.Printer, outputFormat string, nics []iaas.NIC) error p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/network-interface/list/list_test.go b/internal/cmd/network-interface/list/list_test.go index 3e976c5db..3ef5c3397 100644 --- a/internal/cmd/network-interface/list/list_test.go +++ b/internal/cmd/network-interface/list/list_test.go @@ -4,30 +4,35 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() var testLabelSelector = "label" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + networkIdFlag: testNetworkId, limitFlag: "10", labelSelectorFlag: testLabelSelector, @@ -43,6 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), @@ -54,8 +60,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest { - request := testClient.ListNics(testCtx, testProjectId, testNetworkId) +func fixtureProjectRequest(mods ...func(request *iaas.ApiListProjectNICsRequest)) iaas.ApiListProjectNICsRequest { + request := testClient.DefaultAPI.ListProjectNICs(testCtx, testProjectId, testRegion) + request = request.LabelSelector(testLabelSelector) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureNetworkRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest { + request := testClient.DefaultAPI.ListNics(testCtx, testProjectId, testRegion, testNetworkId) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -66,6 +81,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiList func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -89,21 +105,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -135,43 +151,32 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } +func TestBuildProjectRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiListProjectNICsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureProjectRequest(), + }, + } - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildProjectRequest(testCtx, tt.model, testClient) - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -179,7 +184,7 @@ func TestParseInput(t *testing.T) { } } -func TestBuildRequest(t *testing.T) { +func TestBuildNetworkRequest(t *testing.T) { tests := []struct { description string model *inputModel @@ -188,17 +193,17 @@ func TestBuildRequest(t *testing.T) { { description: "base", model: fixtureInputModel(), - expectedRequest: fixtureRequest(), + expectedRequest: fixtureNetworkRequest(), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildNetworkRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -207,7 +212,59 @@ func TestBuildRequest(t *testing.T) { } } -func TestOutputResult(t *testing.T) { +func TestOutputProjectResult(t *testing.T) { + type args struct { + outputFormat string + nics []iaas.NIC + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "nil as NIC-slice", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: false, + }, + { + name: "empty NIC-slice", + args: args{ + outputFormat: print.PrettyOutputFormat, + nics: []iaas.NIC{}, + }, + wantErr: false, + }, + { + name: "empty NIC in NIC-slice", + args: args{ + outputFormat: print.PrettyOutputFormat, + nics: []iaas.NIC{{}}, + }, + wantErr: false, + }, + { + name: "two empty NICs in NIC-slice to verify sorting by network id does not break on nil pointers", + args: args{ + outputFormat: print.PrettyOutputFormat, + nics: []iaas.NIC{{}, {}}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputProjectResult(params.Printer, tt.args.outputFormat, tt.args.nics, ""); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestOutputNetworkResult(t *testing.T) { type args struct { outputFormat string nics []iaas.NIC @@ -218,16 +275,33 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, + name: "nil as NIC-slice", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: false, + }, + { + name: "empty NIC-slice", + args: args{ + outputFormat: print.PrettyOutputFormat, + nics: []iaas.NIC{}, + }, + wantErr: false, + }, + { + name: "empty NIC in NIC-slice", + args: args{ + outputFormat: print.PrettyOutputFormat, + nics: []iaas.NIC{{}}, + }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.nics); (err != nil) != tt.wantErr { + if err := outputNetworkResult(params.Printer, tt.args.outputFormat, tt.args.nics, ""); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network-interface/network-interface.go b/internal/cmd/network-interface/network-interface.go index f9bbca3fe..3b9c2fb48 100644 --- a/internal/cmd/network-interface/network-interface.go +++ b/internal/cmd/network-interface/network-interface.go @@ -2,17 +2,18 @@ package networkinterface import ( "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/cmd/network-interface/create" "github.com/stackitcloud/stackit-cli/internal/cmd/network-interface/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/network-interface/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/network-interface/list" "github.com/stackitcloud/stackit-cli/internal/cmd/network-interface/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "network-interface", Short: "Provides functionality for network interfaces", @@ -24,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) diff --git a/internal/cmd/network-interface/update/update.go b/internal/cmd/network-interface/update/update.go index 92b3e02ab..6673cada5 100644 --- a/internal/cmd/network-interface/update/update.go +++ b/internal/cmd/network-interface/update/update.go @@ -2,13 +2,14 @@ package update import ( "context" - "encoding/json" "fmt" "regexp" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -40,15 +40,15 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel NicId string - NetworkId *string - AllowedAddresses *[]iaas.AllowedAddressesInner - Labels *map[string]string + NetworkId string + AllowedAddresses []iaas.AllowedAddressesInner + Labels map[string]any Name *string // <= 63 characters + regex ^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$ NicSecurity *bool - SecurityGroups *[]string // = 36 characters + regex ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ + SecurityGroups []string // = 36 characters + regex ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", nicIdArg), Short: "Updates a network interface", @@ -81,12 +81,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update the network interface %q?", model.NicId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update the network interface %q?", model.NicId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -150,11 +148,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } // check security groups size and regex - securityGroups := flags.FlagToStringSlicePointer(p, cmd, securityGroupsFlag) - if securityGroups != nil && len(*securityGroups) > 0 { + securityGroups := flags.FlagToStringSliceValue(p, cmd, securityGroupsFlag) + if len(securityGroups) > 0 { securityGroupsRegex := regexp.MustCompile(securityGroupsRegex) // iterate over them - for _, value := range *securityGroups { + for _, value := range securityGroups { if len(value) != securityGroupLength { return nil, &errors.FlagValidationError{ Flag: securityGroupsFlag, @@ -173,35 +171,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, NicId: nicId, - NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + NetworkId: flags.FlagToStringValue(p, cmd, networkIdFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), Name: name, NicSecurity: flags.FlagToBoolPointer(p, cmd, nicSecurityFlag), SecurityGroups: securityGroups, } if allowedAddresses != nil { - model.AllowedAddresses = utils.Ptr(allowedAddressesInner) - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + model.AllowedAddresses = allowedAddressesInner } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateNicRequest { - req := apiClient.UpdateNic(ctx, model.ProjectId, *model.NetworkId, model.NicId) + req := apiClient.DefaultAPI.UpdateNic(ctx, model.ProjectId, model.Region, model.NetworkId, model.NicId) payload := iaas.UpdateNicPayload{ AllowedAddresses: model.AllowedAddresses, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, Name: model.Name, NicSecurity: model.NicSecurity, SecurityGroups: model.SecurityGroups, @@ -213,25 +203,8 @@ func outputResult(p *print.Printer, outputFormat, projectId string, nic *iaas.NI if nic == nil { return fmt.Errorf("nic is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(nic, "", " ") - if err != nil { - return fmt.Errorf("marshal network interface: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(nic, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network interface: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, nic, func() error { p.Outputf("Updated network interface for project %q.\n", projectId) return nil - } + }) } diff --git a/internal/cmd/network-interface/update/update_test.go b/internal/cmd/network-interface/update/update_test.go index 03faa73ad..474d52f8a 100644 --- a/internal/cmd/network-interface/update/update_test.go +++ b/internal/cmd/network-interface/update/update_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() var testNicId = uuid.NewString() @@ -38,7 +39,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + networkIdFlag: testNetworkId, allowedAddressesFlag: "1.1.1.1,8.8.8.8,9.9.9.9", labelFlag: "key=value", @@ -53,7 +56,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st } func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { - var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{ + var allowedAddresses = []iaas.AllowedAddressesInner{ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")), iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")), iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")), @@ -62,15 +65,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - NetworkId: utils.Ptr(testNetworkId), - AllowedAddresses: utils.Ptr(allowedAddresses), - Labels: utils.Ptr(map[string]string{ + NetworkId: testNetworkId, + AllowedAddresses: allowedAddresses, + Labels: map[string]any{ "key": "value", - }), + }, Name: utils.Ptr("testNic"), NicSecurity: utils.Ptr(true), - SecurityGroups: utils.Ptr([]string{testSecurityGroup}), + SecurityGroups: []string{testSecurityGroup}, NicId: testNicId, } for _, mod := range mods { @@ -80,7 +84,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateNicRequest)) iaas.ApiUpdateNicRequest { - request := testClient.UpdateNic(testCtx, testProjectId, testNetworkId, testNicId) + request := testClient.DefaultAPI.UpdateNic(testCtx, testProjectId, testRegion, testNetworkId, testNicId) request = request.UpdateNicPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -89,19 +93,19 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdateNicRequest)) iaas.ApiUpd } func fixturePayload(mods ...func(payload *iaas.UpdateNicPayload)) iaas.UpdateNicPayload { - var allowedAddresses []iaas.AllowedAddressesInner = []iaas.AllowedAddressesInner{ + var allowedAddresses = []iaas.AllowedAddressesInner{ iaas.StringAsAllowedAddressesInner(utils.Ptr("1.1.1.1")), iaas.StringAsAllowedAddressesInner(utils.Ptr("8.8.8.8")), iaas.StringAsAllowedAddressesInner(utils.Ptr("9.9.9.9")), } payload := iaas.UpdateNicPayload{ - AllowedAddresses: utils.Ptr(allowedAddresses), - Labels: utils.Ptr(map[string]interface{}{ + AllowedAddresses: allowedAddresses, + Labels: map[string]any{ "key": "value", - }), + }, Name: utils.Ptr("testNic"), NicSecurity: utils.Ptr(true), - SecurityGroups: utils.Ptr([]string{testSecurityGroup}), + SecurityGroups: []string{testSecurityGroup}, } for _, mod := range mods { mod(&payload) @@ -213,8 +217,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -246,7 +250,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -284,7 +288,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -317,11 +321,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectId, tt.args.nic); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectId, tt.args.nic); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network/create/create.go b/internal/cmd/network/create/create.go index ce5871590..1aae3c8f9 100644 --- a/internal/cmd/network/create/create.go +++ b/internal/cmd/network/create/create.go @@ -2,11 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -36,27 +36,29 @@ const ( nonRoutedFlag = "non-routed" noIpv4GatewayFlag = "no-ipv4-gateway" noIpv6GatewayFlag = "no-ipv6-gateway" + routingTableIdFlag = "routing-table-id" labelFlag = "labels" ) type inputModel struct { *globalflags.GlobalFlagModel - Name *string - IPv4DnsNameServers *[]string + Name string + IPv4DnsNameServers []string IPv4PrefixLength *int64 IPv4Prefix *string IPv4Gateway *string - IPv6DnsNameServers *[]string + IPv6DnsNameServers []string IPv6PrefixLength *int64 IPv6Prefix *string IPv6Gateway *string NonRouted bool NoIPv4Gateway bool NoIPv6Gateway bool - Labels *map[string]string + RoutingTableID *string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a network", @@ -81,16 +83,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { ), examples.NewExample( `Create an IPv4 network with name "network-1" with DNS name servers, a prefix and a gateway`, - `$ stackit network create --name network-1 --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" --ipv4-gateway "10.1.2.3"`, + `$ stackit network create --name network-1 --non-routed --ipv4-dns-name-servers "1.1.1.1,8.8.8.8,9.9.9.9" --ipv4-prefix "10.1.2.0/24" --ipv4-gateway "10.1.2.3"`, ), examples.NewExample( `Create an IPv6 network with name "network-1" with DNS name servers, a prefix and a gateway`, `$ stackit network create --name network-1 --ipv6-dns-name-servers "2001:4860:4860::8888,2001:4860:4860::8844" --ipv6-prefix "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888"`, ), + examples.NewExample( + `Create a network with name "network-1" and attach routing-table "xxx"`, + `$ stackit network create --name network-1 --routing-table-id xxx`, + ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -109,12 +115,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a network for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a network for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -123,19 +127,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create network : %w", err) } - networkId := *resp.NetworkId + + if resp == nil { + return fmt.Errorf("create network : empty response") + } + networkId := resp.Id // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating network") - _, err = wait.CreateNetworkWaitHandler(ctx, apiClient, model.ProjectId, networkId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating network", func() error { + _, err = wait.CreateNetworkWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, networkId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for network creation: %w", err) } - s.Stop() } - return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) }, } @@ -156,13 +163,25 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(nonRoutedFlag, false, "If set to true, the network is not routed and therefore not accessible from other networks") cmd.Flags().Bool(noIpv4GatewayFlag, false, "If set to true, the network doesn't have an IPv4 gateway") cmd.Flags().Bool(noIpv6GatewayFlag, false, "If set to true, the network doesn't have an IPv6 gateway") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "The ID of the routing-table for the network") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network. E.g. '--labels key1=value1,key2=value2,...'") + // IPv4 checks + cmd.MarkFlagsMutuallyExclusive(ipv4PrefixFlag, ipv4PrefixLengthFlag) + cmd.MarkFlagsMutuallyExclusive(ipv4GatewayFlag, ipv4PrefixLengthFlag) + cmd.MarkFlagsMutuallyExclusive(ipv4GatewayFlag, noIpv4GatewayFlag) + cmd.MarkFlagsMutuallyExclusive(noIpv4GatewayFlag, ipv4PrefixLengthFlag) + + // IPv6 checks + cmd.MarkFlagsMutuallyExclusive(ipv6PrefixFlag, ipv6PrefixLengthFlag) + cmd.MarkFlagsMutuallyExclusive(ipv6GatewayFlag, ipv6PrefixLengthFlag) + cmd.MarkFlagsMutuallyExclusive(ipv6GatewayFlag, noIpv6GatewayFlag) + err := flags.MarkFlagsRequired(cmd, nameFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -170,78 +189,119 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - Name: flags.FlagToStringPointer(p, cmd, nameFlag), - IPv4DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, ipv4DnsNameServersFlag), + Name: flags.FlagToStringValue(p, cmd, nameFlag), + IPv4DnsNameServers: flags.FlagToStringSliceValue(p, cmd, ipv4DnsNameServersFlag), IPv4PrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv4PrefixLengthFlag), IPv4Prefix: flags.FlagToStringPointer(p, cmd, ipv4PrefixFlag), IPv4Gateway: flags.FlagToStringPointer(p, cmd, ipv4GatewayFlag), - IPv6DnsNameServers: flags.FlagToStringSlicePointer(p, cmd, ipv6DnsNameServersFlag), + + IPv6DnsNameServers: flags.FlagToStringSliceValue(p, cmd, ipv6DnsNameServersFlag), IPv6PrefixLength: flags.FlagToInt64Pointer(p, cmd, ipv6PrefixLengthFlag), IPv6Prefix: flags.FlagToStringPointer(p, cmd, ipv6PrefixFlag), IPv6Gateway: flags.FlagToStringPointer(p, cmd, ipv6GatewayFlag), NonRouted: flags.FlagToBoolValue(p, cmd, nonRoutedFlag), NoIPv4Gateway: flags.FlagToBoolValue(p, cmd, noIpv4GatewayFlag), NoIPv6Gateway: flags.FlagToBoolValue(p, cmd, noIpv6GatewayFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + RoutingTableID: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + // IPv4 nameserver can not be set alone. IPv4 Prefix || IPv4 Prefix length must be set as well + isIPv4NameserverSet := len(model.IPv4DnsNameServers) > 0 + isIPv4PrefixOrPrefixLengthSet := model.IPv4Prefix != nil || model.IPv4PrefixLength != nil + if isIPv4NameserverSet && !isIPv4PrefixOrPrefixLengthSet { + return nil, &cliErr.OneOfFlagsIsMissing{ + MissingFlags: []string{ipv4PrefixLengthFlag, ipv4PrefixFlag}, + SetFlag: ipv4DnsNameServersFlag, + } + } + isIPv4GatewaySet := model.IPv4Gateway != nil + isIPv4PrefixSet := model.IPv4Prefix != nil + if isIPv4GatewaySet && !isIPv4PrefixSet { + return nil, &cliErr.DependingFlagIsMissing{ + MissingFlag: ipv4PrefixFlag, + SetFlag: ipv4GatewayFlag, } } + // IPv6 nameserver can not be set alone. IPv6 Prefix || IPv6 Prefix length must be set as well + isIPv6NameserverSet := len(model.IPv6DnsNameServers) > 0 + isIPv6PrefixOrPrefixLengthSet := model.IPv6Prefix != nil || model.IPv6PrefixLength != nil + if isIPv6NameserverSet && !isIPv6PrefixOrPrefixLengthSet { + return nil, &cliErr.OneOfFlagsIsMissing{ + MissingFlags: []string{ipv6PrefixLengthFlag, ipv6PrefixFlag}, + SetFlag: ipv6DnsNameServersFlag, + } + } + isIPv6GatewaySet := model.IPv6Gateway != nil + isIPv6PrefixSet := model.IPv6Prefix != nil + if isIPv6GatewaySet && !isIPv6PrefixSet { + return nil, &cliErr.DependingFlagIsMissing{ + MissingFlag: ipv6PrefixFlag, + SetFlag: ipv6GatewayFlag, + } + } + + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateNetworkRequest { - req := apiClient.CreateNetwork(ctx, model.ProjectId) - addressFamily := &iaas.CreateNetworkAddressFamily{} - - if model.IPv6DnsNameServers != nil || model.IPv6PrefixLength != nil || model.IPv6Prefix != nil || model.NoIPv6Gateway || model.IPv6Gateway != nil { - addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{ - Nameservers: model.IPv6DnsNameServers, - PrefixLength: model.IPv6PrefixLength, - Prefix: model.IPv6Prefix, + req := apiClient.DefaultAPI.CreateNetwork(ctx, model.ProjectId, model.Region) + var ipv4Network *iaas.CreateNetworkIPv4 + var ipv6Network *iaas.CreateNetworkIPv6 + + if model.IPv6Prefix != nil { + ipv6Network = &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ + Prefix: *model.IPv6Prefix, + Nameservers: model.IPv6DnsNameServers, + }, } if model.NoIPv6Gateway { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) + ipv6Network.CreateNetworkIPv6WithPrefix.Gateway = *iaas.NewNullableString(nil) } else if model.IPv6Gateway != nil { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(model.IPv6Gateway) + ipv6Network.CreateNetworkIPv6WithPrefix.Gateway = *iaas.NewNullableString(model.IPv6Gateway) + } + } else if model.IPv6PrefixLength != nil { + ipv6Network = &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefixLength: &iaas.CreateNetworkIPv6WithPrefixLength{ + PrefixLength: *model.IPv6PrefixLength, + Nameservers: model.IPv6DnsNameServers, + }, } } - if model.IPv4DnsNameServers != nil || model.IPv4PrefixLength != nil || model.IPv4Prefix != nil || model.NoIPv4Gateway || model.IPv4Gateway != nil { - addressFamily.Ipv4 = &iaas.CreateNetworkIPv4Body{ - Nameservers: model.IPv4DnsNameServers, - PrefixLength: model.IPv4PrefixLength, - Prefix: model.IPv4Prefix, + if model.IPv4Prefix != nil { + ipv4Network = &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Prefix: *model.IPv4Prefix, + Nameservers: model.IPv4DnsNameServers, + }, } if model.NoIPv4Gateway { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil) + ipv4Network.CreateNetworkIPv4WithPrefix.Gateway = *iaas.NewNullableString(nil) } else if model.IPv4Gateway != nil { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(model.IPv4Gateway) + ipv4Network.CreateNetworkIPv4WithPrefix.Gateway = *iaas.NewNullableString(model.IPv4Gateway) + } + } else if model.IPv4PrefixLength != nil { + ipv4Network = &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefixLength: &iaas.CreateNetworkIPv4WithPrefixLength{ + PrefixLength: *model.IPv4PrefixLength, + Nameservers: model.IPv4DnsNameServers, + }, } - } - - routed := true - if model.NonRouted { - routed = false } payload := iaas.CreateNetworkPayload{ - Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - Routed: &routed, - } - - if addressFamily.Ipv4 != nil || addressFamily.Ipv6 != nil { - payload.AddressFamily = addressFamily + Name: model.Name, + Labels: model.Labels, + Routed: utils.Ptr(!model.NonRouted), + Ipv4: ipv4Network, + Ipv6: ipv6Network, + RoutingTableId: model.RoutingTableID, } return req.CreateNetworkPayload(payload) @@ -251,29 +311,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe if network == nil { return fmt.Errorf("network cannot be nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(network, "", " ") - if err != nil { - return fmt.Errorf("marshal network: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(network, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, network, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } - p.Outputf("%s network for project %q.\nNetwork ID: %s\n", operationState, projectLabel, utils.PtrString(network.NetworkId)) + p.Outputf("%s network for project %q.\nNetwork ID: %s\n", operationState, projectLabel, network.Id) return nil - } + }) } diff --git a/internal/cmd/network/create/create_test.go b/internal/cmd/network/create/create_test.go index 63c6a3635..37aec4fb0 100644 --- a/internal/cmd/network/create/create_test.go +++ b/internal/cmd/network/create/create_test.go @@ -2,42 +2,57 @@ package create import ( "context" + "strconv" + "strings" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" -type testCtxKey struct{} + testNetworkName = "example-network-name" + testIPv4PrefixLength int64 = 24 + testIPv4Prefix = "10.1.2.0/24" + testIPv4Gateway = "10.1.2.3" + testIPv6PrefixLength int64 = 24 + testIPv6Prefix = "2001:4860:4860::/64" + testIPv6Gateway = "2001:db8:0:8d3:0:8a2e:70:1" + testNonRouted = false +) -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var ( + testIPv4NameServers = []string{"1.1.1.0", "1.1.2.0"} + testIPv6NameServers = []string{"2001:4860:4860::8888", "2001:4860:4860::8844"} +) -var testProjectId = uuid.NewString() +type testCtxKey struct{} + +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRoutingTableId = uuid.NewString() +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - nameFlag: "example-network-name", - ipv4DnsNameServersFlag: "1.1.1.0,1.1.2.0", - ipv4PrefixLengthFlag: "24", - ipv4PrefixFlag: "10.1.2.0/24", - ipv4GatewayFlag: "10.1.2.3", - ipv6DnsNameServersFlag: "2001:4860:4860::8888,2001:4860:4860::8844", - ipv6PrefixLengthFlag: "24", - ipv6PrefixFlag: "2001:4860:4860::8888", - ipv6GatewayFlag: "2001:4860:4860::8888", - nonRoutedFlag: "false", - labelFlag: "key=value", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + nameFlag: testNetworkName, + nonRoutedFlag: strconv.FormatBool(testNonRouted), + labelFlag: "key=value", + routingTableIdFlag: testRoutingTableId, } for _, mod := range mods { mod(flagValues) @@ -45,26 +60,82 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st return flagValues } +func fixtureFlagValuesWithPrefix(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4DnsNameServersFlag] = strings.Join(testIPv4NameServers, ",") + flagValues[ipv4PrefixFlag] = testIPv4Prefix + flagValues[ipv4GatewayFlag] = testIPv4Gateway + + flagValues[ipv6DnsNameServersFlag] = strings.Join(testIPv6NameServers, ",") + flagValues[ipv6PrefixFlag] = testIPv6Prefix + flagValues[ipv6GatewayFlag] = testIPv6Gateway + }) + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureFlagValuesWithPrefixLength(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4PrefixLengthFlag] = strconv.FormatInt(testIPv4PrefixLength, 10) + flagValues[ipv4DnsNameServersFlag] = strings.Join(testIPv4NameServers, ",") + + flagValues[ipv6PrefixLengthFlag] = strconv.FormatInt(testIPv6PrefixLength, 10) + flagValues[ipv6DnsNameServersFlag] = strings.Join(testIPv6NameServers, ",") + }) + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-name"), - IPv4DnsNameServers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - IPv4PrefixLength: utils.Ptr(int64(24)), - IPv4Prefix: utils.Ptr("10.1.2.0/24"), - IPv4Gateway: utils.Ptr("10.1.2.3"), - IPv6DnsNameServers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}), - IPv6PrefixLength: utils.Ptr(int64(24)), - IPv6Prefix: utils.Ptr("2001:4860:4860::8888"), - IPv6Gateway: utils.Ptr("2001:4860:4860::8888"), - NonRouted: false, - Labels: utils.Ptr(map[string]string{ + Name: testNetworkName, + NonRouted: testNonRouted, + Labels: map[string]any{ "key": "value", - }), + }, + RoutingTableID: utils.Ptr(testRoutingTableId), + } + for _, mod := range mods { + mod(model) } + return model +} + +func fixtureInputModelWithPrefix(mods ...func(model *inputModel)) *inputModel { + model := fixtureInputModel() + + model.IPv4DnsNameServers = testIPv4NameServers + model.IPv4Prefix = utils.Ptr(testIPv4Prefix) + model.IPv4Gateway = utils.Ptr(testIPv4Gateway) + + model.IPv6DnsNameServers = testIPv6NameServers + model.IPv6Prefix = utils.Ptr(testIPv6Prefix) + model.IPv6Gateway = utils.Ptr(testIPv6Gateway) + + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureInputModelWithPrefixLength(mods ...func(model *inputModel)) *inputModel { + model := fixtureInputModel() + + model.IPv4DnsNameServers = testIPv4NameServers + model.IPv4PrefixLength = utils.Ptr(testIPv4PrefixLength) + + model.IPv6DnsNameServers = testIPv6NameServers + model.IPv6PrefixLength = utils.Ptr(testIPv6PrefixLength) + for _, mod := range mods { mod(model) } @@ -72,7 +143,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkRequest)) iaas.ApiCreateNetworkRequest { - request := testClient.CreateNetwork(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateNetwork(testCtx, testProjectId, testRegion) request = request.CreateNetworkPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -81,9 +152,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateNetworkRequest)) iaas.Ap } func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateNetworkRequest)) iaas.ApiCreateNetworkRequest { - request := testClient.CreateNetwork(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateNetwork(testCtx, testProjectId, testRegion) request = request.CreateNetworkPayload(iaas.CreateNetworkPayload{ - Name: utils.Ptr("example-network-name"), + Name: testNetworkName, Routed: utils.Ptr(true), }) for _, mod := range mods { @@ -94,24 +165,53 @@ func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateNetworkRequest)) func fixturePayload(mods ...func(payload *iaas.CreateNetworkPayload)) iaas.CreateNetworkPayload { payload := iaas.CreateNetworkPayload{ - Name: utils.Ptr("example-network-name"), + Name: "example-network-name", Routed: utils.Ptr(true), - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Nameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - PrefixLength: utils.Ptr(int64(24)), - Prefix: utils.Ptr("10.1.2.0/24"), - Gateway: iaas.NewNullableString(utils.Ptr("10.1.2.3")), - }, - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}), - PrefixLength: utils.Ptr(int64(24)), - Prefix: utils.Ptr("2001:4860:4860::8888"), - Gateway: iaas.NewNullableString(utils.Ptr("2001:4860:4860::8888")), - }, + }, + RoutingTableId: utils.Ptr(testRoutingTableId), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixturePayloadWithPrefix(mods ...func(payload *iaas.CreateNetworkPayload)) iaas.CreateNetworkPayload { + payload := fixturePayload() + payload.Ipv4 = &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefix: &iaas.CreateNetworkIPv4WithPrefix{ + Gateway: *iaas.NewNullableString(utils.Ptr(testIPv4Gateway)), + Nameservers: testIPv4NameServers, + Prefix: testIPv4Prefix, + }, + } + payload.Ipv6 = &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ + Nameservers: testIPv6NameServers, + Prefix: testIPv6Prefix, + Gateway: *iaas.NewNullableString(utils.Ptr(testIPv6Gateway)), + }, + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixturePayloadWithPrefixLength(mods ...func(payload *iaas.CreateNetworkPayload)) iaas.CreateNetworkPayload { + payload := fixturePayload() + payload.Ipv4 = &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefixLength: &iaas.CreateNetworkIPv4WithPrefixLength{ + PrefixLength: testIPv4PrefixLength, + Nameservers: testIPv4NameServers, + }, + } + payload.Ipv6 = &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefixLength: &iaas.CreateNetworkIPv6WithPrefixLength{ + PrefixLength: testIPv6PrefixLength, + Nameservers: testIPv6NameServers, }, } for _, mod := range mods { @@ -123,6 +223,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateNetworkPayload)) iaas.Creat func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -135,15 +236,21 @@ func TestParseInput(t *testing.T) { }, { description: "required only", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, ipv4DnsNameServersFlag) - delete(flagValues, ipv4PrefixLengthFlag) - }), + flagValues: map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + nameFlag: testNetworkName, + }, isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.IPv4DnsNameServers = nil - model.IPv4PrefixLength = nil - }), + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + Region: testRegion, + }, + Name: testNetworkName, + }, }, { description: "name missing", @@ -160,66 +267,110 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, { - description: "use dns servers, prefix, gateway and prefix length", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[ipv4DnsNameServersFlag] = "1.1.1.1" - flagValues[ipv4PrefixLengthFlag] = "25" - flagValues[ipv4PrefixFlag] = "10.1.2.0/24" - flagValues[ipv4GatewayFlag] = "10.1.2.3" + description: "use with prefix", + flagValues: fixtureFlagValuesWithPrefix(), + isValid: true, + expectedModel: fixtureInputModelWithPrefix(), + }, + { + description: "use with prefix only ipv4", + flagValues: fixtureFlagValuesWithPrefix(func(flagValues map[string]string) { + delete(flagValues, ipv6GatewayFlag) + delete(flagValues, ipv6PrefixFlag) + delete(flagValues, ipv6PrefixLengthFlag) + delete(flagValues, ipv6DnsNameServersFlag) }), isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.IPv4DnsNameServers = utils.Ptr([]string{"1.1.1.1"}) - model.IPv4PrefixLength = utils.Ptr(int64(25)) - model.IPv4Prefix = utils.Ptr("10.1.2.0/24") - model.IPv4Gateway = utils.Ptr("10.1.2.3") + expectedModel: fixtureInputModelWithPrefix(func(model *inputModel) { + model.IPv6PrefixLength = nil + model.IPv6Prefix = nil + model.IPv6DnsNameServers = nil + model.IPv6Gateway = nil }), }, { - description: "use ipv4 gateway nil", - flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[noIpv4GatewayFlag] = "true" + description: "use with prefix only ipv6", + flagValues: fixtureFlagValuesWithPrefix(func(flagValues map[string]string) { delete(flagValues, ipv4GatewayFlag) + delete(flagValues, ipv4PrefixFlag) + delete(flagValues, ipv4PrefixLengthFlag) + delete(flagValues, ipv4DnsNameServersFlag) }), isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.NoIPv4Gateway = true + expectedModel: fixtureInputModelWithPrefix(func(model *inputModel) { + model.IPv4PrefixLength = nil + model.IPv4Prefix = nil + model.IPv4DnsNameServers = nil model.IPv4Gateway = nil }), }, { - description: "use ipv6 dns servers, prefix, gateway and prefix length", + description: "use with prefixLength", + flagValues: fixtureFlagValuesWithPrefixLength(), + isValid: true, + expectedModel: fixtureInputModelWithPrefixLength(), + }, + { + description: "use with prefixLength only ipv4", + flagValues: fixtureFlagValuesWithPrefixLength(func(flagValues map[string]string) { + delete(flagValues, ipv6GatewayFlag) + delete(flagValues, ipv6PrefixFlag) + delete(flagValues, ipv6PrefixLengthFlag) + delete(flagValues, ipv6DnsNameServersFlag) + }), + isValid: true, + expectedModel: fixtureInputModelWithPrefixLength(func(model *inputModel) { + model.IPv6PrefixLength = nil + model.IPv6Prefix = nil + model.IPv6DnsNameServers = nil + model.IPv6Gateway = nil + }), + }, + { + description: "use with prefixLength only ipv6", + flagValues: fixtureFlagValuesWithPrefixLength(func(flagValues map[string]string) { + delete(flagValues, ipv4GatewayFlag) + delete(flagValues, ipv4PrefixFlag) + delete(flagValues, ipv4PrefixLengthFlag) + delete(flagValues, ipv4DnsNameServersFlag) + }), + isValid: true, + expectedModel: fixtureInputModelWithPrefixLength(func(model *inputModel) { + model.IPv4PrefixLength = nil + model.IPv4Prefix = nil + model.IPv4DnsNameServers = nil + model.IPv4Gateway = nil + }), + }, + { + description: "use ipv4 gateway nil", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[ipv6DnsNameServersFlag] = "2001:4860:4860::8888" - flagValues[ipv6PrefixLengthFlag] = "25" - flagValues[ipv6PrefixFlag] = "2001:4860:4860::8888" - flagValues[ipv6GatewayFlag] = "2001:4860:4860::8888" + flagValues[noIpv4GatewayFlag] = "true" + delete(flagValues, ipv4GatewayFlag) }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.IPv6DnsNameServers = utils.Ptr([]string{"2001:4860:4860::8888"}) - model.IPv6PrefixLength = utils.Ptr(int64(25)) - model.IPv6Prefix = utils.Ptr("2001:4860:4860::8888") - model.IPv6Gateway = utils.Ptr("2001:4860:4860::8888") + model.NoIPv4Gateway = true + model.IPv4Gateway = nil }), }, { @@ -234,6 +385,72 @@ func TestParseInput(t *testing.T) { model.IPv6Gateway = nil }), }, + { + description: "ipv4 prefix length and prefix conflict", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4PrefixFlag] = testIPv4Prefix + flagValues[ipv4PrefixLengthFlag] = strconv.FormatInt(testIPv4PrefixLength, 10) + }), + isValid: false, + expectedModel: nil, + }, + { + description: "ipv6 prefix length and prefix conflict", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv6PrefixFlag] = testIPv6Prefix + flagValues[ipv6PrefixLengthFlag] = strconv.FormatInt(testIPv6PrefixLength, 10) + }), + isValid: false, + expectedModel: nil, + }, + { + description: "ipv4 nameserver with missing prefix or prefix length", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4DnsNameServersFlag] = strings.Join(testIPv4NameServers, ",") + }), + isValid: false, + expectedModel: nil, + }, + { + description: "ipv6 nameserver with missing prefix or prefix length", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv6DnsNameServersFlag] = strings.Join(testIPv6NameServers, ",") + }), + isValid: false, + expectedModel: nil, + }, + { + description: "ipv4 gateway and no-gateway flag conflict", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4GatewayFlag] = testIPv4Gateway + flagValues[noIpv4GatewayFlag] = "true" + }), + isValid: false, + }, + { + description: "ipv6 gateway and no-gateway flag conflict", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv6GatewayFlag] = testIPv4Gateway + flagValues[noIpv6GatewayFlag] = "true" + }), + isValid: false, + }, + { + description: "ipv4 gateway and prefixLength flag conflict", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv4GatewayFlag] = testIPv4Gateway + flagValues[ipv4PrefixLengthFlag] = strconv.FormatInt(testIPv4PrefixLength, 10) + }), + isValid: false, + }, + { + description: "ipv6 gateway and prefixLength flag conflict", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[ipv6GatewayFlag] = testIPv6Gateway + flagValues[ipv6PrefixLengthFlag] = strconv.FormatInt(testIPv6PrefixLength, 10) + }), + isValid: false, + }, { description: "non-routed network", flagValues: fixtureFlagValues(func(flagValues map[string]string) { @@ -254,50 +471,29 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "routing-table id invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdFlag] = "invalid-uuid" + }), + expectedModel: nil, + isValid: false, + }, + { + description: "routing-table id not set", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, routingTableIdFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.RoutingTableID = nil + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -319,107 +515,115 @@ func TestBuildRequest(t *testing.T) { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-name"), + Name: testNetworkName, }, expectedRequest: fixtureRequiredRequest(), }, + { + description: "use prefix length", + model: fixtureInputModelWithPrefixLength(), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateNetworkRequest) { + *request = request.CreateNetworkPayload(fixturePayloadWithPrefixLength()) + }), + }, + { + description: "use prefix", + model: fixtureInputModelWithPrefix(), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateNetworkRequest) { + *request = request.CreateNetworkPayload(fixturePayloadWithPrefix()) + }), + }, { description: "non-routed network", model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - Name: utils.Ptr("example-network-name"), + Name: testNetworkName, NonRouted: true, }, - expectedRequest: testClient.CreateNetwork(testCtx, testProjectId).CreateNetworkPayload(iaas.CreateNetworkPayload{ - Name: utils.Ptr("example-network-name"), + expectedRequest: testClient.DefaultAPI.CreateNetwork(testCtx, testProjectId, testRegion).CreateNetworkPayload(iaas.CreateNetworkPayload{ + Name: testNetworkName, Routed: utils.Ptr(false), }), }, { - description: "use dns servers, prefix, gateway and prefix length", + description: "network with routing-table id attached", model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - IPv4DnsNameServers: utils.Ptr([]string{"1.1.1.1"}), - IPv4PrefixLength: utils.Ptr(int64(25)), - IPv4Prefix: utils.Ptr("10.1.2.0/24"), - IPv4Gateway: utils.Ptr("10.1.2.3"), + Name: testNetworkName, + RoutingTableID: utils.Ptr(testRoutingTableId), }, - expectedRequest: testClient.CreateNetwork(testCtx, testProjectId).CreateNetworkPayload(iaas.CreateNetworkPayload{ - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Nameservers: utils.Ptr([]string{"1.1.1.1"}), - PrefixLength: utils.Ptr(int64(25)), - Prefix: utils.Ptr("10.1.2.0/24"), - Gateway: iaas.NewNullableString(utils.Ptr("10.1.2.3")), - }, - }, - Routed: utils.Ptr(true), + expectedRequest: testClient.DefaultAPI.CreateNetwork(testCtx, testProjectId, testRegion).CreateNetworkPayload(iaas.CreateNetworkPayload{ + Name: testNetworkName, + RoutingTableId: utils.Ptr(testRoutingTableId), + Routed: utils.Ptr(true), }), }, { - description: "use ipv4 gateway nil", + description: "use ipv4 dns servers and prefix length", model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - NoIPv4Gateway: true, - IPv4Gateway: nil, + IPv4DnsNameServers: []string{"1.1.1.1"}, + IPv4PrefixLength: utils.Ptr(int64(25)), }, - expectedRequest: testClient.CreateNetwork(testCtx, testProjectId).CreateNetworkPayload(iaas.CreateNetworkPayload{ - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv4: &iaas.CreateNetworkIPv4Body{ - Gateway: iaas.NewNullableString(nil), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateNetworkRequest) { + *request = request.CreateNetworkPayload(iaas.CreateNetworkPayload{ + Ipv4: &iaas.CreateNetworkIPv4{ + CreateNetworkIPv4WithPrefixLength: &iaas.CreateNetworkIPv4WithPrefixLength{ + Nameservers: []string{"1.1.1.1"}, + PrefixLength: int64(25), + }, }, - }, - Routed: utils.Ptr(true), + Routed: utils.Ptr(true), + }) }), }, { - description: "use ipv6 dns servers, prefix, gateway and prefix length", - model: &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ - ProjectId: testProjectId, - Verbosity: globalflags.VerbosityDefault, - }, - IPv6DnsNameServers: utils.Ptr([]string{"2001:4860:4860::8888"}), - IPv6PrefixLength: utils.Ptr(int64(25)), - IPv6Prefix: utils.Ptr("2001:4860:4860::8888"), - IPv6Gateway: utils.Ptr("2001:4860:4860::8888"), - }, - expectedRequest: testClient.CreateNetwork(testCtx, testProjectId).CreateNetworkPayload(iaas.CreateNetworkPayload{ - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{"2001:4860:4860::8888"}), - PrefixLength: utils.Ptr(int64(25)), - Prefix: utils.Ptr("2001:4860:4860::8888"), - Gateway: iaas.NewNullableString(utils.Ptr("2001:4860:4860::8888")), - }, - }, - Routed: utils.Ptr(true), + description: "use prefix with no gateway", + model: fixtureInputModelWithPrefix(func(model *inputModel) { + model.NoIPv4Gateway = true + model.NoIPv6Gateway = true + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateNetworkRequest) { + *request = request.CreateNetworkPayload( + fixturePayloadWithPrefix(func(payload *iaas.CreateNetworkPayload) { + payload.Ipv4.CreateNetworkIPv4WithPrefix.Gateway = *iaas.NewNullableString(nil) + payload.Ipv6.CreateNetworkIPv6WithPrefix.Gateway = *iaas.NewNullableString(nil) + }), + ) }), }, { - description: "use ipv6 gateway nil", + description: "use ipv6 dns servers, prefix and gateway", model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - NoIPv6Gateway: true, - IPv6Gateway: nil, + IPv6DnsNameServers: []string{"2001:4860:4860::8888"}, + IPv6Prefix: utils.Ptr("2001:4860:4860::8888"), + IPv6Gateway: utils.Ptr("2001:4860:4860::8888"), }, - expectedRequest: testClient.CreateNetwork(testCtx, testProjectId).CreateNetworkPayload(iaas.CreateNetworkPayload{ - AddressFamily: &iaas.CreateNetworkAddressFamily{ - Ipv6: &iaas.CreateNetworkIPv6Body{ - Gateway: iaas.NewNullableString(nil), + expectedRequest: testClient.DefaultAPI.CreateNetwork(testCtx, testProjectId, testRegion).CreateNetworkPayload(iaas.CreateNetworkPayload{ + Ipv6: &iaas.CreateNetworkIPv6{ + CreateNetworkIPv6WithPrefix: &iaas.CreateNetworkIPv6WithPrefix{ + Nameservers: []string{"2001:4860:4860::8888"}, + Prefix: "2001:4860:4860::8888", + Gateway: *iaas.NewNullableString(utils.Ptr("2001:4860:4860::8888")), }, }, Routed: utils.Ptr(true), @@ -430,9 +634,9 @@ func TestBuildRequest(t *testing.T) { t.Run(tt.description, func(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) - diff := cmp.Diff(request, tt.expectedRequest, + diff := cmp.Diff(tt.expectedRequest, request, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { @@ -467,11 +671,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.network); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.network); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network/delete/delete.go b/internal/cmd/network/delete/delete.go index 005c3e312..25d05275d 100644 --- a/internal/cmd/network/delete/delete.go +++ b/internal/cmd/network/delete/delete.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ type inputModel struct { NetworkId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", networkIdArg), Short: "Deletes a network", @@ -57,7 +59,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.NetworkId) + networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.NetworkId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) networkLabel = model.NetworkId @@ -65,12 +67,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { networkLabel = model.NetworkId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete network %q?", networkLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete network %q?", networkLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -82,13 +82,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting network") - _, err = wait.DeleteNetworkWaitHandler(ctx, apiClient, model.ProjectId, model.NetworkId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting network", func() error { + _, err = wait.DeleteNetworkWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.NetworkId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for network deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -115,18 +115,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu NetworkId: networkId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteNetworkRequest { - return apiClient.DeleteNetwork(ctx, model.ProjectId, model.NetworkId) + return apiClient.DefaultAPI.DeleteNetwork(ctx, model.ProjectId, model.Region, model.NetworkId) } diff --git a/internal/cmd/network/delete/delete_test.go b/internal/cmd/network/delete/delete_test.go index 630a0f3fb..666fab6a2 100644 --- a/internal/cmd/network/delete/delete_test.go +++ b/internal/cmd/network/delete/delete_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testNetworkId = uuid.NewString() var testProjectId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, NetworkId: testNetworkId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteNetworkRequest)) iaas.ApiDeleteNetworkRequest { - request := testClient.DeleteNetwork(testCtx, testProjectId, testNetworkId) + request := testClient.DefaultAPI.DeleteNetwork(testCtx, testProjectId, testRegion, testNetworkId) for _, mod := range mods { mod(&request) } @@ -102,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/network/describe/describe.go b/internal/cmd/network/describe/describe.go index b4c12a317..f76d18017 100644 --- a/internal/cmd/network/describe/describe.go +++ b/internal/cmd/network/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ type inputModel struct { NetworkId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", networkIdArg), Short: "Shows details of a network", @@ -85,74 +85,62 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu NetworkId: networkId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetNetworkRequest { - return apiClient.GetNetwork(ctx, model.ProjectId, model.NetworkId) + return apiClient.DefaultAPI.GetNetwork(ctx, model.ProjectId, model.Region, model.NetworkId) } func outputResult(p *print.Printer, outputFormat string, network *iaas.Network) error { if network == nil { return fmt.Errorf("network cannot be nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(network, "", " ") - if err != nil { - return fmt.Errorf("marshal network: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(network, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - var ipv4nameservers []string - if network.Nameservers != nil { - ipv4nameservers = append(ipv4nameservers, *network.Nameservers...) - } - - var ipv4prefixes []string - if network.Prefixes != nil { - ipv4prefixes = append(ipv4prefixes, *network.Prefixes...) - } - - var ipv6nameservers []string - if network.NameserversV6 != nil { - ipv6nameservers = append(ipv6nameservers, *network.NameserversV6...) + return p.OutputResult(outputFormat, network, func() error { + // IPv4 + var ipv4Nameservers, ipv4Prefixes []string + var publicIp, ipv4Gateway *string + if ipv4 := network.Ipv4; ipv4 != nil { + if ipv4.Nameservers != nil { + ipv4Nameservers = append(ipv4Nameservers, ipv4.Nameservers...) + } + if ipv4.Prefixes != nil { + ipv4Prefixes = append(ipv4Prefixes, ipv4.Prefixes...) + } + if ipv4.PublicIp != nil { + publicIp = ipv4.PublicIp + } + if ipv4.Gateway.IsSet() { + ipv4Gateway = ipv4.Gateway.Get() + } } - var ipv6prefixes []string - if network.PrefixesV6 != nil { - ipv6prefixes = append(ipv6prefixes, *network.PrefixesV6...) + // IPv6 + var ipv6Nameservers, ipv6Prefixes []string + var ipv6Gateway *string + if ipv6 := network.Ipv6; ipv6 != nil { + if ipv6.Nameservers != nil { + ipv6Nameservers = append(ipv6Nameservers, ipv6.Nameservers...) + } + if ipv6.Prefixes != nil { + ipv6Prefixes = append(ipv6Prefixes, ipv6.Prefixes...) + } + if ipv6.Gateway.IsSet() { + ipv6Gateway = ipv6.Gateway.Get() + } } table := tables.NewTable() - table.AddRow("ID", utils.PtrString(network.NetworkId)) + table.AddRow("ID", network.Id) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(network.Name)) + table.AddRow("NAME", network.Name) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(network.State)) + table.AddRow("STATE", network.Status) table.AddSeparator() - if network.PublicIp != nil { - table.AddRow("PUBLIC IP", *network.PublicIp) + if publicIp != nil { + table.AddRow("PUBLIC IP", *publicIp) table.AddSeparator() } @@ -164,36 +152,41 @@ func outputResult(p *print.Printer, outputFormat string, network *iaas.Network) table.AddRow("ROUTED", routed) table.AddSeparator() - if network.Gateway != nil { - table.AddRow("IPv4 GATEWAY", *network.Gateway.Get()) + if network.RoutingTableId != nil { + table.AddRow("ROUTING TABLE ID", utils.PtrString(network.RoutingTableId)) table.AddSeparator() } - if len(ipv4nameservers) > 0 { - table.AddRow("IPv4 NAME SERVERS", strings.Join(ipv4nameservers, ", ")) + if ipv4Gateway != nil { + table.AddRow("IPv4 GATEWAY", *ipv4Gateway) + table.AddSeparator() + } + + if len(ipv4Nameservers) > 0 { + table.AddRow("IPv4 NAME SERVERS", strings.Join(ipv4Nameservers, ", ")) } table.AddSeparator() - if len(ipv4prefixes) > 0 { - table.AddRow("IPv4 PREFIXES", strings.Join(ipv4prefixes, ", ")) + if len(ipv4Prefixes) > 0 { + table.AddRow("IPv4 PREFIXES", strings.Join(ipv4Prefixes, ", ")) } table.AddSeparator() - if network.Gatewayv6 != nil { - table.AddRow("IPv6 GATEWAY", *network.Gatewayv6.Get()) + if ipv6Gateway != nil { + table.AddRow("IPv6 GATEWAY", *ipv6Gateway) table.AddSeparator() } - if len(ipv6nameservers) > 0 { - table.AddRow("IPv6 NAME SERVERS", strings.Join(ipv6nameservers, ", ")) + if len(ipv6Nameservers) > 0 { + table.AddRow("IPv6 NAME SERVERS", strings.Join(ipv6Nameservers, ", ")) + table.AddSeparator() } - table.AddSeparator() - if len(ipv6prefixes) > 0 { - table.AddRow("IPv6 PREFIXES", strings.Join(ipv6prefixes, ", ")) + if len(ipv6Prefixes) > 0 { + table.AddRow("IPv6 PREFIXES", strings.Join(ipv6Prefixes, ", ")) + table.AddSeparator() } - table.AddSeparator() - if network.Labels != nil && len(*network.Labels) > 0 { + if len(network.Labels) > 0 { var labels []string - for key, value := range *network.Labels { + for key, value := range network.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -205,5 +198,5 @@ func outputResult(p *print.Printer, outputFormat string, network *iaas.Network) return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/network/describe/describe_test.go b/internal/cmd/network/describe/describe_test.go index 9c3b62d35..7494b22f5 100644 --- a/internal/cmd/network/describe/describe_test.go +++ b/internal/cmd/network/describe/describe_test.go @@ -4,22 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() @@ -35,7 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, NetworkId: testNetworkId, } @@ -58,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetNetworkRequest)) iaas.ApiGetNetworkRequest { - request := testClient.GetNetwork(testCtx, testProjectId, testNetworkId) + request := testClient.DefaultAPI.GetNetwork(testCtx, testProjectId, testRegion, testNetworkId) for _, mod := range mods { mod(&request) } @@ -102,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -240,12 +197,29 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "set empty ipv4", + args: args{ + network: &iaas.Network{ + Ipv4: &iaas.NetworkIPv4{}, + }, + }, + wantErr: false, + }, + { + name: "set empty ipv6", + args: args{ + network: &iaas.Network{ + Ipv6: &iaas.NetworkIPv6{}, + }, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.network); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.network); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network/list/list.go b/internal/cmd/network/list/list.go index 2584d7fd7..ad412f36a 100644 --- a/internal/cmd/network/list/list.go +++ b/internal/cmd/network/list/list.go @@ -2,11 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -33,7 +34,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all networks of a project", @@ -57,9 +58,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit network list --label-selector xxx", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -77,25 +78,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list networks: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId - } - params.Printer.Info("No networks found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -107,7 +103,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -127,69 +123,53 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNetworksRequest { - req := apiClient.ListNetworks(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListNetworks(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } return req } -func outputResult(p *print.Printer, outputFormat string, networks []iaas.Network) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(networks, "", " ") - if err != nil { - return fmt.Errorf("marshal network: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, networks []iaas.Network) error { + return p.OutputResult(outputFormat, networks, func() error { + if len(networks) == 0 { + p.Outputf("No networks found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(networks, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal network: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.SetHeader("ID", "NAME", "STATUS", "PUBLIC IP", "PREFIXES", "ROUTED") + table.SetHeader("ID", "NAME", "STATUS", "PUBLIC IP", "PREFIXES", "ROUTED", "ROUTING TABLE ID") for _, network := range networks { - publicIp := utils.PtrString(network.PublicIp) + var publicIp, prefixes string + if ipv4 := network.Ipv4; ipv4 != nil { + publicIp = utils.PtrString(ipv4.PublicIp) + prefixes = strings.Join(ipv4.Prefixes, ", ") + } routed := false if network.Routed != nil { routed = *network.Routed } - prefixes := utils.JoinStringPtr(network.Prefixes, ", ") table.AddRow( - utils.PtrString(network.NetworkId), - utils.PtrString(network.Name), - utils.PtrString(network.State), + network.Id, + network.Name, + network.Status, publicIp, prefixes, routed, + utils.PtrString(network.RoutingTableId), ) table.AddSeparator() } p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/network/list/list_test.go b/internal/cmd/network/list/list_test.go index c0f1ac4c3..ca08245ac 100644 --- a/internal/cmd/network/list/list_test.go +++ b/internal/cmd/network/list/list_test.go @@ -4,29 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" + testLabelSelector = "foo=bar" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testLabelSelector = "foo=bar" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", labelSelectorFlag: testLabelSelector, } @@ -41,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), @@ -52,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListNetworksRequest)) iaas.ApiListNetworksRequest { - request := testClient.ListNetworks(testCtx, testProjectId) + request := testClient.DefaultAPI.ListNetworks(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -63,6 +68,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListNetworksRequest)) iaas.Api func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -86,21 +92,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -132,46 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -195,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -207,6 +174,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string networks []iaas.Network } tests := []struct { @@ -229,11 +197,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.networks); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.networks); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/network/network.go b/internal/cmd/network/network.go index 5fbd7e77b..eb7c6ece7 100644 --- a/internal/cmd/network/network.go +++ b/internal/cmd/network/network.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/network/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/network/list" "github.com/stackitcloud/stackit-cli/internal/cmd/network/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "network", Short: "Provides functionality for networks", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/network/update/update.go b/internal/cmd/network/update/update.go index 712342cfc..eda3bb95d 100644 --- a/internal/cmd/network/update/update.go +++ b/internal/cmd/network/update/update.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,8 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -31,6 +33,7 @@ const ( ipv6GatewayFlag = "ipv6-gateway" noIpv4GatewayFlag = "no-ipv4-gateway" noIpv6GatewayFlag = "no-ipv6-gateway" + routingTableIdFlag = "routing-table-id" labelFlag = "labels" ) @@ -44,10 +47,11 @@ type inputModel struct { IPv6Gateway *string NoIPv4Gateway bool NoIPv6Gateway bool - Labels *map[string]string + RoutingTableId *string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", networkIdArg), Short: "Updates a network", @@ -70,6 +74,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Update IPv6 network with ID "xxx" with new name "network-1-new", new gateway and new DNS name servers`, `$ stackit network update xxx --name network-1-new --ipv6-dns-name-servers "2001:4860:4860::8888" --ipv6-gateway "2001:4860:4860::8888"`, ), + examples.NewExample( + `Update network with ID "xxx" with new routing-table id "xxx"`, + `$ stackit network update xxx --routing-table-id xxx`, + ), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -84,7 +92,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.NetworkId) + networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.NetworkId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) networkLabel = model.NetworkId @@ -92,12 +100,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { networkLabel = model.NetworkId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update network %q?", networkLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update network %q?", networkLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -110,15 +116,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating network") - _, err = wait.UpdateNetworkWaitHandler(ctx, apiClient, model.ProjectId, networkId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating network", func() error { + _, err = wait.UpdateNetworkWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, networkId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for network update: %w", err) } - s.Stop() } - operationState := "Updated" if model.Async { operationState = "Triggered update of" @@ -139,6 +144,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(ipv6GatewayFlag, "", "The IPv6 gateway of a network. If not specified, the first IP of the network will be assigned as the gateway") cmd.Flags().Bool(noIpv4GatewayFlag, false, "If set to true, the network doesn't have an IPv4 gateway") cmd.Flags().Bool(noIpv6GatewayFlag, false, "If set to true, the network doesn't have an IPv6 gateway") + cmd.Flags().Var(flags.UUIDFlag(), routingTableIdFlag, "The ID of the routing-table for the network") cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a network. E.g. '--labels key1=value1,key2=value2,...'") } @@ -160,56 +166,49 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu IPv6Gateway: flags.FlagToStringPointer(p, cmd, ipv6GatewayFlag), NoIPv4Gateway: flags.FlagToBoolValue(p, cmd, noIpv4GatewayFlag), NoIPv6Gateway: flags.FlagToBoolValue(p, cmd, noIpv6GatewayFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + RoutingTableId: flags.FlagToStringPointer(p, cmd, routingTableIdFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiPartialUpdateNetworkRequest { - req := apiClient.PartialUpdateNetwork(ctx, model.ProjectId, model.NetworkId) - addressFamily := &iaas.UpdateNetworkAddressFamily{} + req := apiClient.DefaultAPI.PartialUpdateNetwork(ctx, model.ProjectId, model.Region, model.NetworkId) + var payloadIPv4 *iaas.UpdateNetworkIPv4Body + var payloadIPv6 *iaas.UpdateNetworkIPv6Body if model.IPv6DnsNameServers != nil || model.NoIPv6Gateway || model.IPv6Gateway != nil { - addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{ - Nameservers: model.IPv6DnsNameServers, + payloadIPv6 = &iaas.UpdateNetworkIPv6Body{ + Nameservers: *model.IPv6DnsNameServers, } if model.NoIPv6Gateway { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) + payloadIPv6.Gateway = *iaas.NewNullableString(nil) } else if model.IPv6Gateway != nil { - addressFamily.Ipv6.Gateway = iaas.NewNullableString(model.IPv6Gateway) + payloadIPv6.Gateway = *iaas.NewNullableString(model.IPv6Gateway) } } if model.IPv4DnsNameServers != nil || model.NoIPv4Gateway || model.IPv4Gateway != nil { - addressFamily.Ipv4 = &iaas.UpdateNetworkIPv4Body{ - Nameservers: model.IPv4DnsNameServers, + payloadIPv4 = &iaas.UpdateNetworkIPv4Body{ + Nameservers: *model.IPv4DnsNameServers, } if model.NoIPv4Gateway { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil) + payloadIPv4.Gateway = *iaas.NewNullableString(nil) } else if model.IPv4Gateway != nil { - addressFamily.Ipv4.Gateway = iaas.NewNullableString(model.IPv4Gateway) + payloadIPv4.Gateway = *iaas.NewNullableString(model.IPv4Gateway) } } payload := iaas.PartialUpdateNetworkPayload{ - Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), - } - - if addressFamily.Ipv4 != nil || addressFamily.Ipv6 != nil { - payload.AddressFamily = addressFamily + Name: model.Name, + Ipv4: payloadIPv4, + Ipv6: payloadIPv6, + Labels: model.Labels, + RoutingTableId: model.RoutingTableId, } return req.PartialUpdateNetworkPayload(payload) diff --git a/internal/cmd/network/update/update_test.go b/internal/cmd/network/update/update_test.go index d05624840..44dad9933 100644 --- a/internal/cmd/network/update/update_test.go +++ b/internal/cmd/network/update/update_test.go @@ -4,26 +4,28 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testNetworkId = uuid.NewString() +var testRoutingTableId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -37,13 +39,16 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + nameFlag: "example-network-name", - projectIdFlag: testProjectId, ipv4DnsNameServersFlag: "1.1.1.0,1.1.2.0", ipv4GatewayFlag: "10.1.2.3", ipv6DnsNameServersFlag: "2001:4860:4860::8888,2001:4860:4860::8844", ipv6GatewayFlag: "2001:4860:4860::8888", labelFlag: "key=value", + routingTableIdFlag: testRoutingTableId, } for _, mod := range mods { mod(flagValues) @@ -56,6 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, Name: utils.Ptr("example-network-name"), NetworkId: testNetworkId, @@ -63,9 +69,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { IPv4Gateway: utils.Ptr("10.1.2.3"), IPv6DnsNameServers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}), IPv6Gateway: utils.Ptr("2001:4860:4860::8888"), - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, + RoutingTableId: utils.Ptr(testRoutingTableId), } for _, mod := range mods { mod(model) @@ -74,7 +81,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkRequest)) iaas.ApiPartialUpdateNetworkRequest { - request := testClient.PartialUpdateNetwork(testCtx, testProjectId, testNetworkId) + request := testClient.DefaultAPI.PartialUpdateNetwork(testCtx, testProjectId, testRegion, testNetworkId) request = request.PartialUpdateNetworkPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -85,19 +92,18 @@ func fixtureRequest(mods ...func(request *iaas.ApiPartialUpdateNetworkRequest)) func fixturePayload(mods ...func(payload *iaas.PartialUpdateNetworkPayload)) iaas.PartialUpdateNetworkPayload { payload := iaas.PartialUpdateNetworkPayload{ Name: utils.Ptr("example-network-name"), - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), - AddressFamily: &iaas.UpdateNetworkAddressFamily{ - Ipv4: &iaas.UpdateNetworkIPv4Body{ - Nameservers: utils.Ptr([]string{"1.1.1.0", "1.1.2.0"}), - Gateway: iaas.NewNullableString(utils.Ptr("10.1.2.3")), - }, - Ipv6: &iaas.UpdateNetworkIPv6Body{ - Nameservers: utils.Ptr([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"}), - Gateway: iaas.NewNullableString(utils.Ptr("2001:4860:4860::8888")), - }, }, + Ipv4: &iaas.UpdateNetworkIPv4Body{ + Nameservers: []string{"1.1.1.0", "1.1.2.0"}, + Gateway: *iaas.NewNullableString(utils.Ptr("10.1.2.3")), + }, + Ipv6: &iaas.UpdateNetworkIPv6Body{ + Nameservers: []string{"2001:4860:4860::8888", "2001:4860:4860::8844"}, + Gateway: *iaas.NewNullableString(utils.Ptr("2001:4860:4860::8888")), + }, + RoutingTableId: utils.Ptr(testRoutingTableId), } for _, mod := range mods { mod(&payload) @@ -142,7 +148,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -150,7 +156,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -158,7 +164,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -237,12 +243,21 @@ func TestParseInput(t *testing.T) { }), isValid: true, }, + { + description: "route-table id wrong format", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[routingTableIdFlag] = "wrong-format" + }), + expectedModel: nil, + isValid: false, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -274,7 +289,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -313,7 +328,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), - cmp.AllowUnexported(iaas.NullableString{}), + cmp.AllowUnexported(iaas.NullableString{}, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/object-storage/bucket/bucket.go b/internal/cmd/object-storage/bucket/bucket.go index 62d928e54..0f8ab39a3 100644 --- a/internal/cmd/object-storage/bucket/bucket.go +++ b/internal/cmd/object-storage/bucket/bucket.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/bucket/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/bucket/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/bucket/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "bucket", Short: "Provides functionality for Object Storage buckets", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) diff --git a/internal/cmd/object-storage/bucket/create/create.go b/internal/cmd/object-storage/bucket/create/create.go index fd899a4d2..577d244d0 100644 --- a/internal/cmd/object-storage/bucket/create/create.go +++ b/internal/cmd/object-storage/bucket/create/create.go @@ -2,11 +2,11 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,20 +17,22 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/wait" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api/wait" ) const ( - bucketNameArg = "BUCKET_NAME" + bucketNameArg = "BUCKET_NAME" + objectLockEnabledFlag = "object-lock-enabled" ) type inputModel struct { *globalflags.GlobalFlagModel - BucketName string + BucketName string + ObjectLockEnabled bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("create %s", bucketNameArg), Short: "Creates an Object Storage bucket", @@ -40,6 +42,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { examples.NewExample( `Create an Object Storage bucket with name "my-bucket"`, "$ stackit object-storage bucket create my-bucket"), + examples.NewExample( + `Create an Object Storage bucket with enabled object-lock`, + `$ stackit object-storage bucket create my-bucket --object-lock-enabled`), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -54,16 +59,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create bucket %q? (This cannot be undone)", model.BucketName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create bucket %q? (This cannot be undone)", model.BucketName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Check if the project is enabled before trying to create - enabled, err := utils.ProjectEnabled(ctx, apiClient, model.ProjectId, model.Region) + enabled, err := utils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) if err != nil { return fmt.Errorf("check if Object Storage is enabled: %w", err) } @@ -82,21 +85,26 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating bucket") - _, err = wait.CreateBucketWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.BucketName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating bucket", func() error { + _, err = wait.CreateBucketWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BucketName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Object Storage bucket creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, model.BucketName, resp) }, } + configureFlags(cmd) return cmd } +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(objectLockEnabledFlag, false, "is the object-lock enabled for the bucket") +} + func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { bucketName := inputArgs[0] @@ -106,24 +114,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } model := inputModel{ - GlobalFlagModel: globalFlags, - BucketName: bucketName, - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + GlobalFlagModel: globalFlags, + BucketName: bucketName, + ObjectLockEnabled: flags.FlagToBoolValue(p, cmd, objectLockEnabledFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiCreateBucketRequest { - req := apiClient.CreateBucket(ctx, model.ProjectId, model.Region, model.BucketName) + req := apiClient.DefaultAPI.CreateBucket(ctx, model.ProjectId, model.Region, model.BucketName).ObjectLockEnabled(model.ObjectLockEnabled) return req } @@ -132,29 +133,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, bucketName return fmt.Errorf("create bucket response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage bucket: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage bucket: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } p.Outputf("%s bucket %q\n", operationState, bucketName) return nil - } + }) } diff --git a/internal/cmd/object-storage/bucket/create/create_test.go b/internal/cmd/object-storage/bucket/create/create_test.go index 344c228d5..bb45d7685 100644 --- a/internal/cmd/object-storage/bucket/create/create_test.go +++ b/internal/cmd/object-storage/bucket/create/create_test.go @@ -4,26 +4,26 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" -var testBucketName = "my-bucket" + +const ( + testRegion = "eu01" + testBucketName = "my-bucket" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -37,8 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -53,7 +53,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, Region: testRegion, }, - BucketName: testBucketName, + BucketName: testBucketName, + ObjectLockEnabled: false, } for _, mod := range mods { mod(model) @@ -61,10 +62,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *objectstorage.ApiCreateBucketRequest)) objectstorage.ApiCreateBucketRequest { - request := testClient.CreateBucket(testCtx, testProjectId, testRegion, testBucketName) +func fixtureRequest(mods ...func(request objectstorage.ApiCreateBucketRequest) objectstorage.ApiCreateBucketRequest) objectstorage.ApiCreateBucketRequest { + request := testClient.DefaultAPI.CreateBucket(testCtx, testProjectId, testRegion, testBucketName).ObjectLockEnabled(false) for _, mod := range mods { - mod(&request) + request = mod(request) } return request } @@ -106,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -114,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -122,7 +123,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -132,58 +133,22 @@ func TestParseInput(t *testing.T) { flagValues: fixtureFlagValues(), isValid: false, }, + { + description: "enable object-lock", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[objectLockEnabledFlag] = "true" + }), + expectedModel: fixtureInputModel(func(model *inputModel) { + model.ObjectLockEnabled = true + }), + isValid: true, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,6 +164,15 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), }, + { + description: "object-lock enabled", + model: fixtureInputModel(func(model *inputModel) { + model.ObjectLockEnabled = true + }), + expectedRequest: fixtureRequest(func(request objectstorage.ApiCreateBucketRequest) objectstorage.ApiCreateBucketRequest { + return request.ObjectLockEnabled(true) + }), + }, } for _, tt := range tests { @@ -206,7 +180,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -241,11 +215,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.bucketName, tt.args.createBucketResponse); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.bucketName, tt.args.createBucketResponse); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/bucket/delete/delete.go b/internal/cmd/object-storage/bucket/delete/delete.go index fa4e68e99..c6cc396f8 100644 --- a/internal/cmd/object-storage/bucket/delete/delete.go +++ b/internal/cmd/object-storage/bucket/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/wait" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api/wait" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { BucketName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", bucketNameArg), Short: "Deletes an Object Storage bucket", @@ -51,12 +52,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete bucket %q? (This cannot be undone)", model.BucketName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete bucket %q? (This cannot be undone)", model.BucketName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -68,13 +67,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting bucket") - _, err = wait.DeleteBucketWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.BucketName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting bucket", func() error { + _, err = wait.DeleteBucketWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BucketName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Object Storage bucket deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -101,19 +100,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BucketName: bucketName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiDeleteBucketRequest { - req := apiClient.DeleteBucket(ctx, model.ProjectId, model.Region, model.BucketName) + req := apiClient.DefaultAPI.DeleteBucket(ctx, model.ProjectId, model.Region, model.BucketName) return req } diff --git a/internal/cmd/object-storage/bucket/delete/delete_test.go b/internal/cmd/object-storage/bucket/delete/delete_test.go index 6e06d4adc..2bc6f06be 100644 --- a/internal/cmd/object-storage/bucket/delete/delete_test.go +++ b/internal/cmd/object-storage/bucket/delete/delete_test.go @@ -4,26 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" -var testBucketName = "my-bucket" + +const ( + testRegion = "eu01" + testBucketName = "my-bucket" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -37,8 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiDeleteBucketRequest)) objectstorage.ApiDeleteBucketRequest { - request := testClient.DeleteBucket(testCtx, testProjectId, testRegion, testBucketName) + request := testClient.DefaultAPI.DeleteBucket(testCtx, testProjectId, testRegion, testBucketName) for _, mod := range mods { mod(&request) } @@ -106,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -114,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -122,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -136,54 +135,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -206,7 +158,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/bucket/describe/describe.go b/internal/cmd/object-storage/bucket/describe/describe.go index 4112c0674..1375ff05a 100644 --- a/internal/cmd/object-storage/bucket/describe/describe.go +++ b/internal/cmd/object-storage/bucket/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,10 +13,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) const ( @@ -29,7 +27,7 @@ type inputModel struct { BucketName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", bucketNameArg), Short: "Shows details of an Object Storage bucket", @@ -62,7 +60,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read Object Storage bucket: %w", err) } - return outputResult(params.Printer, model.OutputFormat, resp.Bucket) + return outputResult(params.Printer, model.OutputFormat, resp) }, } return cmd @@ -81,54 +79,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BucketName: bucketName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiGetBucketRequest { - req := apiClient.GetBucket(ctx, model.ProjectId, model.Region, model.BucketName) + req := apiClient.DefaultAPI.GetBucket(ctx, model.ProjectId, model.Region, model.BucketName) return req } -func outputResult(p *print.Printer, outputFormat string, bucket *objectstorage.Bucket) error { - if bucket == nil { - return fmt.Errorf("bucket is empty") +func outputResult(p *print.Printer, outputFormat string, resp *objectstorage.GetBucketResponse) error { + if resp == nil { + return fmt.Errorf("response is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(bucket, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage bucket: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(bucket, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage bucket: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp.Bucket, func() error { table := tables.NewTable() - table.AddRow("Name", utils.PtrString(bucket.Name)) + table.AddRow("Name", resp.Bucket.Name) + table.AddSeparator() + table.AddRow("Region", resp.Bucket.Region) table.AddSeparator() - table.AddRow("Region", utils.PtrString(bucket.Region)) + table.AddRow("URL (Path Style)", resp.Bucket.UrlPathStyle) table.AddSeparator() - table.AddRow("URL (Path Style)", utils.PtrString(bucket.UrlPathStyle)) + table.AddRow("URL (Virtual Hosted Style)", resp.Bucket.UrlVirtualHostedStyle) table.AddSeparator() - table.AddRow("URL (Virtual Hosted Style)", utils.PtrString(bucket.UrlVirtualHostedStyle)) + table.AddRow("Object Lock Enabled", resp.Bucket.ObjectLockEnabled) table.AddSeparator() err := table.Display(p) if err != nil { @@ -136,5 +111,5 @@ func outputResult(p *print.Printer, outputFormat string, bucket *objectstorage.B } return nil - } + }) } diff --git a/internal/cmd/object-storage/bucket/describe/describe_test.go b/internal/cmd/object-storage/bucket/describe/describe_test.go index 9e376132d..8be5ab6c8 100644 --- a/internal/cmd/object-storage/bucket/describe/describe_test.go +++ b/internal/cmd/object-storage/bucket/describe/describe_test.go @@ -4,26 +4,26 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" -var testBucketName = "my-bucket" + +const ( + testRegion = "eu01" + testBucketName = "my-bucket" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -37,8 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -62,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiGetBucketRequest)) objectstorage.ApiGetBucketRequest { - request := testClient.GetBucket(testCtx, testProjectId, testRegion, testBucketName) + request := testClient.DefaultAPI.GetBucket(testCtx, testProjectId, testRegion, testBucketName) for _, mod := range mods { mod(&request) } @@ -106,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -114,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -122,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -136,54 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -206,7 +159,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -219,7 +172,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string - bucket *objectstorage.Bucket + resp *objectstorage.GetBucketResponse } tests := []struct { name string @@ -232,18 +185,17 @@ func TestOutputResult(t *testing.T) { wantErr: true, }, { - name: "set empty bucket", + name: "set empty response", args: args{ - bucket: &objectstorage.Bucket{}, + resp: &objectstorage.GetBucketResponse{}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.bucket); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/bucket/list/list.go b/internal/cmd/object-storage/bucket/list/list.go index 8060094c1..352841fa6 100644 --- a/internal/cmd/object-storage/bucket/list/list.go +++ b/internal/cmd/object-storage/bucket/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Object Storage buckets", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Object Storage buckets`, "$ stackit object-storage bucket list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,23 +65,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get Object Storage buckets: %w", err) } - if resp.Buckets == nil || len(*resp.Buckets) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No buckets found for project %s\n", projectLabel) - return nil + buckets := resp.GetBuckets() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - buckets := *resp.Buckets // Truncate output if model.Limit != nil && len(buckets) > int(*model.Limit) { buckets = buckets[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, buckets) + return outputResult(params.Printer, model.OutputFormat, projectLabel, buckets) }, } @@ -94,7 +90,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,55 +109,36 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiListBucketsRequest { - req := apiClient.ListBuckets(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListBuckets(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, buckets []objectstorage.Bucket) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, buckets []objectstorage.Bucket) error { if buckets == nil { return fmt.Errorf("buckets is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(buckets, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage bucket list: %w", err) + return p.OutputResult(outputFormat, buckets, func() error { + if len(buckets) == 0 { + p.Outputf("No buckets found for project %s\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(buckets, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage bucket list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.SetHeader("NAME", "REGION", "URL (PATH STYLE)", "URL (VIRTUAL HOSTED STYLE)") + table.SetHeader("NAME", "REGION", "URL (PATH STYLE)", "URL (VIRTUAL HOSTED STYLE)", "OBJECT LOCK ENABLED") for i := range buckets { bucket := buckets[i] table.AddRow( - utils.PtrString(bucket.Name), - utils.PtrString(bucket.Region), - utils.PtrString(bucket.UrlPathStyle), - utils.PtrString(bucket.UrlVirtualHostedStyle), + bucket.Name, + bucket.Region, + bucket.UrlPathStyle, + bucket.UrlVirtualHostedStyle, + bucket.ObjectLockEnabled, ) } err := table.Display(p) @@ -170,5 +147,5 @@ func outputResult(p *print.Printer, outputFormat string, buckets []objectstorage } return nil - } + }) } diff --git a/internal/cmd/object-storage/bucket/list/list_test.go b/internal/cmd/object-storage/bucket/list/list_test.go index eb50f0ca7..4f2431f70 100644 --- a/internal/cmd/object-storage/bucket/list/list_test.go +++ b/internal/cmd/object-storage/bucket/list/list_test.go @@ -4,33 +4,29 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -54,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiListBucketsRequest)) objectstorage.ApiListBucketsRequest { - request := testClient.ListBuckets(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListBuckets(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -64,6 +60,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiListBucketsRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -82,21 +79,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -118,48 +115,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -182,7 +138,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -195,6 +151,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string buckets []objectstorage.Bucket } tests := []struct { @@ -215,11 +172,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.buckets); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.buckets); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/compliance-lock/compliance-lock.go b/internal/cmd/object-storage/compliance-lock/compliance-lock.go new file mode 100644 index 000000000..49df4e178 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/compliance-lock.go @@ -0,0 +1,30 @@ +package compliancelock + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock/lock" + "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock/unlock" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "compliance-lock", + Short: "Provides functionality to manage Object Storage compliance lock", + Long: "Provides functionality to manage Object Storage compliance lock.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(lock.NewCmd(params)) + cmd.AddCommand(unlock.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) +} diff --git a/internal/cmd/object-storage/compliance-lock/describe/describe.go b/internal/cmd/object-storage/compliance-lock/describe/describe.go new file mode 100644 index 000000000..2aa3ea34a --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/describe/describe.go @@ -0,0 +1,111 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" + objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describe object storage compliance lock", + Long: "Describe object storage compliance lock.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Describe object storage compliance lock`, + "$ stackit object-storage compliance-lock describe"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Check if the project is enabled before trying to describe + enabled, err := objectStorageUtils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + if err != nil { + return fmt.Errorf("check if Object Storage is enabled: %w", err) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "object-storage", + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get object storage compliance lock: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiGetComplianceLockRequest { + req := apiClient.DefaultAPI.GetComplianceLock(ctx, model.ProjectId, model.Region) + return req +} + +func outputResult(p *print.Printer, outputFormat string, resp *objectstorage.ComplianceLockResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("response is empty") + } + + table := tables.NewTable() + table.AddRow("PROJECT ID", resp.Project) + table.AddSeparator() + table.AddRow("MAX RETENTION DAYS", resp.MaxRetentionDays) + table.AddSeparator() + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + }) +} diff --git a/internal/cmd/object-storage/compliance-lock/describe/describe_test.go b/internal/cmd/object-storage/compliance-lock/describe/describe_test.go new file mode 100644 index 000000000..499e7de44 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/describe/describe_test.go @@ -0,0 +1,177 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *objectstorage.ApiGetComplianceLockRequest)) objectstorage.ApiGetComplianceLockRequest { + request := testClient.DefaultAPI.GetComplianceLock(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest objectstorage.ApiGetComplianceLockRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + complianceLock *objectstorage.ComplianceLockResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: true, + }, + { + name: "set empty compliance lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{}, + }, + wantErr: false, + }, + { + name: "set filled lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{ + Project: uuid.New().String(), + MaxRetentionDays: int32(42), + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.complianceLock); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/object-storage/compliance-lock/lock/lock.go b/internal/cmd/object-storage/compliance-lock/lock/lock.go new file mode 100644 index 000000000..e179f7c26 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/lock/lock.go @@ -0,0 +1,116 @@ +package lock + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "lock", + Short: "Create object storage compliance lock", + Long: "Create object storage compliance lock.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Create object storage compliance lock`, + "$ stackit object-storage compliance-lock lock"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to create object storage compliance-lock for project %s?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Check if the project is enabled before trying to create + enabled, err := utils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + if err != nil { + return fmt.Errorf("check if Object Storage is enabled: %w", err) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "object-storage", + } + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("create object storage compliance lock: %w", err) + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiCreateComplianceLockRequest { + req := apiClient.DefaultAPI.CreateComplianceLock(ctx, model.ProjectId, model.Region) + return req +} + +func outputResult(p *print.Printer, outputFormat, projectLabel string, resp *objectstorage.ComplianceLockResponse) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("create compliance lock response is empty") + } + + p.Outputf("Created object storage compliance lock for project \"%s\" with maximum retention period of %d days.\n", projectLabel, resp.MaxRetentionDays) + return nil + }) +} diff --git a/internal/cmd/object-storage/compliance-lock/lock/lock_test.go b/internal/cmd/object-storage/compliance-lock/lock/lock_test.go new file mode 100644 index 000000000..de9610f19 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/lock/lock_test.go @@ -0,0 +1,178 @@ +package lock + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *objectstorage.ApiCreateComplianceLockRequest)) objectstorage.ApiCreateComplianceLockRequest { + request := testClient.DefaultAPI.CreateComplianceLock(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest objectstorage.ApiCreateComplianceLockRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + complianceLock *objectstorage.ComplianceLockResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + }, + wantErr: true, + }, + { + name: "set empty compliance lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{}, + }, + wantErr: false, + }, + { + name: "set filled lock", + args: args{ + outputFormat: print.PrettyOutputFormat, + complianceLock: &objectstorage.ComplianceLockResponse{ + Project: uuid.New().String(), + MaxRetentionDays: int32(42), + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.complianceLock); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/object-storage/compliance-lock/unlock/unlock.go b/internal/cmd/object-storage/compliance-lock/unlock/unlock.go new file mode 100644 index 000000000..ad9320c2a --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/unlock/unlock.go @@ -0,0 +1,106 @@ +package unlock + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "unlock", + Short: "Delete object storage compliance lock", + Long: "Delete object storage compliance lock.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete object storage compliance lock`, + "$ stackit object-storage compliance-lock unlock"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId + } + + prompt := fmt.Sprintf("Are you sure you want to delete object storage compliance-lock for project %s?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Check if the project is enabled before trying to create + enabled, err := utils.ProjectEnabled(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) + if err != nil { + return fmt.Errorf("check if Object Storage is enabled: %w", err) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "object-storage", + } + } + + // Call API + _, err = buildRequest(ctx, model, apiClient).Execute() + if err != nil { + return fmt.Errorf("delete object storage compliance lock: %w", err) + } + + params.Printer.Outputf("Deleted object storage compliance lock for project \"%s\".\n", projectLabel) + + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiDeleteComplianceLockRequest { + req := apiClient.DefaultAPI.DeleteComplianceLock(ctx, model.ProjectId, model.Region) + return req +} diff --git a/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go b/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go new file mode 100644 index 000000000..1df9eb3a9 --- /dev/null +++ b/internal/cmd/object-storage/compliance-lock/unlock/unlock_test.go @@ -0,0 +1,128 @@ +package unlock + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} +var testProjectId = uuid.NewString() + +const ( + testRegion = "eu01" +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *objectstorage.ApiDeleteComplianceLockRequest)) objectstorage.ApiDeleteComplianceLockRequest { + request := testClient.DefaultAPI.DeleteComplianceLock(testCtx, testProjectId, testRegion) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, nil, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest objectstorage.ApiDeleteComplianceLockRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/object-storage/credentials-group/create/create.go b/internal/cmd/object-storage/credentials-group/create/create.go index dac9c248c..07623da46 100644 --- a/internal/cmd/object-storage/credentials-group/create/create.go +++ b/internal/cmd/object-storage/credentials-group/create/create.go @@ -2,11 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,10 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) const ( @@ -29,7 +27,7 @@ type inputModel struct { CredentialsGroupName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a credentials group to hold Object Storage access credentials", @@ -40,9 +38,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials group to hold Object Storage access credentials`, "$ stackit object-storage credentials-group create --name example"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -53,12 +51,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a credentials group with name %q?", model.CredentialsGroupName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a credentials group with name %q?", model.CredentialsGroupName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -82,7 +78,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -93,54 +89,29 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { CredentialsGroupName: flags.FlagToStringValue(p, cmd, credentialsGroupNameFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiCreateCredentialsGroupRequest { - req := apiClient.CreateCredentialsGroup(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.CreateCredentialsGroup(ctx, model.ProjectId, model.Region) req = req.CreateCredentialsGroupPayload(objectstorage.CreateCredentialsGroupPayload{ - DisplayName: utils.Ptr(model.CredentialsGroupName), + DisplayName: model.CredentialsGroupName, }) return req } func outputResult(p *print.Printer, outputFormat string, resp *objectstorage.CreateCredentialsGroupResponse) error { - if resp == nil || resp.CredentialsGroup == nil { - return fmt.Errorf("create createndials group response is empty") - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage credentials group: %w", err) + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("create credentials group response is empty") } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage credentials group: %w", err) - } - p.Outputln(string(details)) - - return nil - default: p.Outputf("Created credentials group %q. Credentials group ID: %s\n\n", - utils.PtrString(resp.CredentialsGroup.DisplayName), - utils.PtrString(resp.CredentialsGroup.CredentialsGroupId), + resp.CredentialsGroup.DisplayName, + resp.CredentialsGroup.CredentialsGroupId, ) - p.Outputf("URN: %s\n", utils.PtrString(resp.CredentialsGroup.Urn)) + p.Outputf("URN: %s\n", resp.CredentialsGroup.Urn) return nil - } + }) } diff --git a/internal/cmd/object-storage/credentials-group/create/create_test.go b/internal/cmd/object-storage/credentials-group/create/create_test.go index f23487603..6d3a47aad 100644 --- a/internal/cmd/object-storage/credentials-group/create/create_test.go +++ b/internal/cmd/object-storage/credentials-group/create/create_test.go @@ -4,33 +4,32 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" -) + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testCredentialsGroupName = "test-name" -var testRegion = "eu01" + +const ( + testCredentialsGroupName = "test-name" + testRegion = "eu01" +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - credentialsGroupNameFlag: testCredentialsGroupName, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + credentialsGroupNameFlag: testCredentialsGroupName, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -55,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixturePayload(mods ...func(payload *objectstorage.CreateCredentialsGroupPayload)) objectstorage.CreateCredentialsGroupPayload { payload := objectstorage.CreateCredentialsGroupPayload{ - DisplayName: utils.Ptr(testCredentialsGroupName), + DisplayName: testCredentialsGroupName, } for _, mod := range mods { mod(&payload) @@ -64,7 +63,7 @@ func fixturePayload(mods ...func(payload *objectstorage.CreateCredentialsGroupPa } func fixtureRequest(mods ...func(request *objectstorage.ApiCreateCredentialsGroupRequest)) objectstorage.ApiCreateCredentialsGroupRequest { - request := testClient.CreateCredentialsGroup(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateCredentialsGroup(testCtx, testProjectId, testRegion) request = request.CreateCredentialsGroupPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -75,6 +74,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiCreateCredentialsGrou func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -93,21 +93,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -122,46 +122,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -184,7 +145,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -214,23 +175,22 @@ func TestOutputResult(t *testing.T) { args: args{ createCredentialsGroupResponse: &objectstorage.CreateCredentialsGroupResponse{}, }, - wantErr: true, + wantErr: false, }, { name: "set create credentials group response", args: args{ createCredentialsGroupResponse: &objectstorage.CreateCredentialsGroupResponse{ - CredentialsGroup: &objectstorage.CredentialsGroup{}, + CredentialsGroup: objectstorage.CredentialsGroup{}, }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.createCredentialsGroupResponse); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.createCredentialsGroupResponse); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/credentials-group/credentials_group.go b/internal/cmd/object-storage/credentials-group/credentials_group.go index 9b9d05129..e9ce52dbd 100644 --- a/internal/cmd/object-storage/credentials-group/credentials_group.go +++ b/internal/cmd/object-storage/credentials-group/credentials_group.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials-group/create" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials-group/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials-group/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials-group", Short: "Provides functionality for Object Storage credentials group", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) diff --git a/internal/cmd/object-storage/credentials-group/delete/delete.go b/internal/cmd/object-storage/credentials-group/delete/delete.go index ff880c0f7..ac14221e1 100644 --- a/internal/cmd/object-storage/credentials-group/delete/delete.go +++ b/internal/cmd/object-storage/credentials-group/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { CredentialsGroupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsGroupIdArg), Short: "Deletes a credentials group that holds Object Storage access credentials", @@ -51,18 +52,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient, model.ProjectId, model.CredentialsGroupId, model.Region) + credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.CredentialsGroupId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials group name: %v", err) credentialsGroupLabel = model.CredentialsGroupId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials group %q? (This cannot be undone)", credentialsGroupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials group %q? (This cannot be undone)", credentialsGroupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -92,19 +91,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsGroupId: credentialsGroupId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiDeleteCredentialsGroupRequest { - req := apiClient.DeleteCredentialsGroup(ctx, model.ProjectId, model.Region, model.CredentialsGroupId) + req := apiClient.DefaultAPI.DeleteCredentialsGroup(ctx, model.ProjectId, model.Region, model.CredentialsGroupId) return req } diff --git a/internal/cmd/object-storage/credentials-group/delete/delete_test.go b/internal/cmd/object-storage/credentials-group/delete/delete_test.go index a5097a0b7..e2c2e33d1 100644 --- a/internal/cmd/object-storage/credentials-group/delete/delete_test.go +++ b/internal/cmd/object-storage/credentials-group/delete/delete_test.go @@ -4,26 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -37,8 +34,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -62,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiDeleteCredentialsGroupRequest)) objectstorage.ApiDeleteCredentialsGroupRequest { - request := testClient.DeleteCredentialsGroup(testCtx, testProjectId, testRegion, testCredentialsGroupId) + request := testClient.DefaultAPI.DeleteCredentialsGroup(testCtx, testProjectId, testRegion, testCredentialsGroupId) for _, mod := range mods { mod(&request) } @@ -106,7 +103,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -114,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -122,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -142,54 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -212,7 +162,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/credentials-group/list/list.go b/internal/cmd/object-storage/credentials-group/list/list.go index ccea6fc84..6c777f0ca 100644 --- a/internal/cmd/object-storage/credentials-group/list/list.go +++ b/internal/cmd/object-storage/credentials-group/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) const ( @@ -29,7 +28,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials groups that hold Object Storage access credentials", @@ -46,9 +45,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials groups`, "$ stackit object-storage credentials-group list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -65,11 +64,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list Object Storage credentials groups: %w", err) } - credentialsGroups := *resp.CredentialsGroups - if len(credentialsGroups) == 0 { - params.Printer.Info("No credentials groups found for your project") - return nil - } + credentialsGroups := resp.GetCredentialsGroups() // Truncate output if model.Limit != nil && len(credentialsGroups) > int(*model.Limit) { @@ -86,7 +81,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -105,50 +100,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiListCredentialsGroupsRequest { - req := apiClient.ListCredentialsGroups(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListCredentialsGroups(ctx, model.ProjectId, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, credentialsGroups []objectstorage.CredentialsGroup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentialsGroups, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage credentials group list: %w", err) + return p.OutputResult(outputFormat, credentialsGroups, func() error { + if len(credentialsGroups) == 0 { + p.Outputf("No credentials groups found for your project") + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentialsGroups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage credentials group list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "URN") for i := range credentialsGroups { c := credentialsGroups[i] table.AddRow( - utils.PtrString(c.CredentialsGroupId), - utils.PtrString(c.DisplayName), - utils.PtrString(c.Urn), + c.CredentialsGroupId, + c.DisplayName, + c.Urn, ) } err := table.Display(p) @@ -156,5 +131,5 @@ func outputResult(p *print.Printer, outputFormat string, credentialsGroups []obj return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/object-storage/credentials-group/list/list_test.go b/internal/cmd/object-storage/credentials-group/list/list_test.go index b0570483b..08b9dfa73 100644 --- a/internal/cmd/object-storage/credentials-group/list/list_test.go +++ b/internal/cmd/object-storage/credentials-group/list/list_test.go @@ -4,32 +4,30 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", - regionFlag: "eu01", + globalflags.ProjectIdFlag: testProjectId, + limitFlag: "10", + globalflags.RegionFlag: "eu01", } for _, mod := range mods { mod(flagValues) @@ -53,7 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiListCredentialsGroupsRequest)) objectstorage.ApiListCredentialsGroupsRequest { - request := testClient.ListCredentialsGroups(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListCredentialsGroups(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +61,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiListCredentialsGroups func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -81,21 +80,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -117,46 +116,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +139,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -219,11 +179,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentialsGroups); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentialsGroups); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/credentials/create/create.go b/internal/cmd/object-storage/credentials/create/create.go index eaef679f5..8a849f26e 100644 --- a/internal/cmd/object-storage/credentials/create/create.go +++ b/internal/cmd/object-storage/credentials/create/create.go @@ -2,13 +2,14 @@ package create import ( "context" - "encoding/json" "fmt" "time" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { HidePassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for an Object Storage credentials group", @@ -48,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for a credentials group with ID "xxx", including a specific expiration date`, "$ stackit object-storage credentials create --credentials-group-id xxx --expire-date 2024-03-06T00:00:00.000Z"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -61,18 +60,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient, model.ProjectId, model.CredentialsGroupId, model.Region) + credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.CredentialsGroupId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials group name: %v", err) credentialsGroupLabel = model.CredentialsGroupId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials in group %q?", credentialsGroupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials in group %q?", credentialsGroupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -97,7 +94,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -117,20 +114,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { CredentialsGroupId: flags.FlagToStringValue(p, cmd, credentialsGroupIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiCreateAccessKeyRequest { - req := apiClient.CreateAccessKey(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.CreateAccessKey(ctx, model.ProjectId, model.Region) req = req.CredentialsGroup(model.CredentialsGroupId) req = req.CreateAccessKeyPayload(objectstorage.CreateAccessKeyPayload{ Expires: model.ExpireDate, @@ -143,34 +132,17 @@ func outputResult(p *print.Printer, outputFormat, credentialsGroupLabel string, return fmt.Errorf("create access key response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { expireDate := "Never" - if resp.Expires != nil && *resp.Expires != "" { - expireDate = *resp.Expires + if resp.Expires.IsSet() && resp.Expires.Get() != nil && *resp.Expires.Get() != "" { + expireDate = *resp.Expires.Get() } - p.Outputf("Created credentials in group %q. Credentials ID: %s\n\n", credentialsGroupLabel, utils.PtrString(resp.KeyId)) - p.Outputf("Access Key ID: %s\n", utils.PtrString(resp.AccessKey)) - p.Outputf("Secret Access Key: %s\n", utils.PtrString(resp.SecretAccessKey)) + p.Outputf("Created credentials in group %q. Credentials ID: %s\n\n", credentialsGroupLabel, resp.KeyId) + p.Outputf("Access Key ID: %s\n", resp.AccessKey) + p.Outputf("Secret Access Key: %s\n", resp.SecretAccessKey) p.Outputf("Expire Date: %s\n", expireDate) return nil - } + }) } diff --git a/internal/cmd/object-storage/credentials/create/create_test.go b/internal/cmd/object-storage/credentials/create/create_test.go index c2efbf566..1cbf8fc7c 100644 --- a/internal/cmd/object-storage/credentials/create/create_test.go +++ b/internal/cmd/object-storage/credentials/create/create_test.go @@ -5,35 +5,35 @@ import ( "testing" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() -var testExpirationDate = "2024-01-01T00:00:00Z" -var testRegion = "eu01" + +const ( + testExpirationDate = "2024-01-01T00:00:00Z" + testRegion = "eu01" +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - credentialsGroupIdFlag: testCredentialsGroupId, - expireDateFlag: testExpirationDate, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + credentialsGroupIdFlag: testCredentialsGroupId, + expireDateFlag: testExpirationDate, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -77,7 +77,7 @@ func fixturePayload(mods ...func(payload *objectstorage.CreateAccessKeyPayload)) } func fixtureRequest(mods ...func(request *objectstorage.ApiCreateAccessKeyRequest)) objectstorage.ApiCreateAccessKeyRequest { - request := testClient.CreateAccessKey(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateAccessKey(testCtx, testProjectId, testRegion) request = request.CreateAccessKeyPayload(fixturePayload()) request = request.CredentialsGroup(testCredentialsGroupId) for _, mod := range mods { @@ -89,6 +89,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiCreateAccessKeyReques func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -107,21 +108,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -181,46 +182,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -243,7 +205,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -276,12 +238,20 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "minimal", + args: args{ + createAccessKeyResponse: &objectstorage.CreateAccessKeyResponse{ + Expires: *objectstorage.NewNullableString(nil), + }, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentialsGroupLabel, tt.args.createAccessKeyResponse); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentialsGroupLabel, tt.args.createAccessKeyResponse); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/credentials/credentials.go b/internal/cmd/object-storage/credentials/credentials.go index 8654a6734..4a271019e 100644 --- a/internal/cmd/object-storage/credentials/credentials.go +++ b/internal/cmd/object-storage/credentials/credentials.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials/create" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for Object Storage credentials", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) diff --git a/internal/cmd/object-storage/credentials/delete/delete.go b/internal/cmd/object-storage/credentials/delete/delete.go index dea8797b0..bc707923a 100644 --- a/internal/cmd/object-storage/credentials/delete/delete.go +++ b/internal/cmd/object-storage/credentials/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) const ( @@ -28,7 +30,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsIdArg), Short: "Deletes credentials of an Object Storage credentials group", @@ -52,24 +54,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient, model.ProjectId, model.CredentialsGroupId, model.Region) + credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.CredentialsGroupId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials group name: %v", err) credentialsGroupLabel = model.CredentialsGroupId } - credentialsLabel, err := objectStorageUtils.GetCredentialsName(ctx, apiClient, model.ProjectId, model.CredentialsGroupId, model.CredentialsId, model.Region) + credentialsLabel, err := objectStorageUtils.GetCredentialsName(ctx, apiClient.DefaultAPI, model.ProjectId, model.CredentialsGroupId, model.CredentialsId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials name: %v", err) credentialsLabel = model.CredentialsId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %q of credentials group %q? (This cannot be undone)", credentialsLabel, credentialsGroupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %q of credentials group %q? (This cannot be undone)", credentialsLabel, credentialsGroupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -108,20 +108,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiDeleteAccessKeyRequest { - req := apiClient.DeleteAccessKey(ctx, model.ProjectId, model.Region, model.CredentialsId) + req := apiClient.DefaultAPI.DeleteAccessKey(ctx, model.ProjectId, model.Region, model.CredentialsId) req = req.CredentialsGroup(model.CredentialsGroupId) return req } diff --git a/internal/cmd/object-storage/credentials/delete/delete_test.go b/internal/cmd/object-storage/credentials/delete/delete_test.go index 1ba508df1..12be8797a 100644 --- a/internal/cmd/object-storage/credentials/delete/delete_test.go +++ b/internal/cmd/object-storage/credentials/delete/delete_test.go @@ -4,27 +4,26 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() -var testCredentialsId = "keyID" -var testRegion = "eu01" + +const ( + testCredentialsId = "keyID" + testRegion = "eu01" +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -38,9 +37,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - credentialsGroupIdFlag: testCredentialsGroupId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + credentialsGroupIdFlag: testCredentialsGroupId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -65,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiDeleteAccessKeyRequest)) objectstorage.ApiDeleteAccessKeyRequest { - request := testClient.DeleteAccessKey(testCtx, testProjectId, testRegion, testCredentialsId) + request := testClient.DefaultAPI.DeleteAccessKey(testCtx, testProjectId, testRegion, testCredentialsId) request = request.CredentialsGroup(testCredentialsGroupId) for _, mod := range mods { mod(&request) @@ -110,7 +109,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -118,7 +117,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -126,7 +125,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -161,54 +160,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -231,7 +183,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/credentials/list/list.go b/internal/cmd/object-storage/credentials/list/list.go index 211ac8d77..119de7c76 100644 --- a/internal/cmd/object-storage/credentials/list/list.go +++ b/internal/cmd/object-storage/credentials/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials for an Object Storage credentials group", @@ -49,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials for a credentials group with ID "xxx"`, "$ stackit object-storage credentials list --credentials-group-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,23 +67,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list Object Storage credentials: %w", err) } - credentials := *resp.AccessKeys - if len(credentials) == 0 { - credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient, model.ProjectId, model.CredentialsGroupId, model.Region) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get credentials group name: %v", err) - credentialsGroupLabel = model.CredentialsGroupId - } - - params.Printer.Info("No credentials found for credentials group %q\n", credentialsGroupLabel) - return nil + credentials := resp.GetAccessKeys() + + credentialsGroupLabel, err := objectStorageUtils.GetCredentialsGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.CredentialsGroupId, model.Region) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get credentials group name: %v", err) + credentialsGroupLabel = model.CredentialsGroupId } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + return outputResult(params.Printer, model.OutputFormat, credentialsGroupLabel, credentials) }, } configureFlags(cmd) @@ -99,7 +94,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -119,55 +114,38 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiListAccessKeysRequest { - req := apiClient.ListAccessKeys(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListAccessKeys(ctx, model.ProjectId, model.Region) req = req.CredentialsGroup(model.CredentialsGroupId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []objectstorage.AccessKey) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal Object Storage credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Object Storage credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, credentialsGroupLabel string, credentials []objectstorage.AccessKey) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for credentials group %q\n", credentialsGroupLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("CREDENTIALS ID", "ACCESS KEY ID", "EXPIRES AT") for i := range credentials { c := credentials[i] - expiresAt := utils.PtrStringDefault(c.Expires, "Never") - table.AddRow(utils.PtrString(c.KeyId), utils.PtrString(c.DisplayName), expiresAt) + expiresAt := "Never" + if c.Expires != "" { + expiresAt = c.Expires + } + table.AddRow(c.KeyId, c.DisplayName, expiresAt) } err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/object-storage/credentials/list/list_test.go b/internal/cmd/object-storage/credentials/list/list_test.go index 6f6fb41a3..ca8cdce84 100644 --- a/internal/cmd/object-storage/credentials/list/list_test.go +++ b/internal/cmd/object-storage/credentials/list/list_test.go @@ -4,34 +4,31 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() var testCredentialsGroupId = uuid.NewString() var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - credentialsGroupIdFlag: testCredentialsGroupId, - limitFlag: "10", - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + credentialsGroupIdFlag: testCredentialsGroupId, + limitFlag: "10", + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -56,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiListAccessKeysRequest)) objectstorage.ApiListAccessKeysRequest { - request := testClient.ListAccessKeys(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListAccessKeys(testCtx, testProjectId, testRegion) request = request.CredentialsGroup(testCredentialsGroupId) for _, mod := range mods { mod(&request) @@ -67,6 +64,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiListAccessKeysRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -85,21 +83,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -142,46 +140,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +163,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -216,8 +175,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - credentials []objectstorage.AccessKey + outputFormat string + credentialsGroupLabel string + credentials []objectstorage.AccessKey } tests := []struct { name string @@ -244,11 +204,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentialsGroupLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/object-storage/disable/disable.go b/internal/cmd/object-storage/disable/disable.go index f6b8ee848..22eba8a93 100644 --- a/internal/cmd/object-storage/disable/disable.go +++ b/internal/cmd/object-storage/disable/disable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,14 +15,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "disable", Short: "Disables Object Storage for a project", @@ -32,9 +33,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Disable Object Storage functionality for your project.`, "$ stackit object-storage disable"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -51,12 +52,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disable Object Storage for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disable Object Storage for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -77,7 +76,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -87,19 +86,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiDisableServiceRequest { - req := apiClient.DisableService(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.DisableService(ctx, model.ProjectId, model.Region) return req } diff --git a/internal/cmd/object-storage/disable/disable_test.go b/internal/cmd/object-storage/disable/disable_test.go index cb65e8961..9f6ce3322 100644 --- a/internal/cmd/object-storage/disable/disable_test.go +++ b/internal/cmd/object-storage/disable/disable_test.go @@ -5,29 +5,26 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -50,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiDisableServiceRequest)) objectstorage.ApiDisableServiceRequest { - request := testClient.DisableService(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.DisableService(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +57,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiDisableServiceRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +76,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -100,46 +98,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -162,7 +121,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/enable/enable.go b/internal/cmd/object-storage/enable/enable.go index 6a064bb3c..0018d56d9 100644 --- a/internal/cmd/object-storage/enable/enable.go +++ b/internal/cmd/object-storage/enable/enable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,14 +15,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "enable", Short: "Enables Object Storage for a project", @@ -32,9 +33,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Enable Object Storage functionality for your project`, "$ stackit object-storage enable"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -51,12 +52,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to enable Object Storage for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to enable Object Storage for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -77,7 +76,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -87,19 +86,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *objectstorage.APIClient) objectstorage.ApiEnableServiceRequest { - req := apiClient.EnableService(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.EnableService(ctx, model.ProjectId, model.Region) return req } diff --git a/internal/cmd/object-storage/enable/enable_test.go b/internal/cmd/object-storage/enable/enable_test.go index 562bb6907..936b2d0f9 100644 --- a/internal/cmd/object-storage/enable/enable_test.go +++ b/internal/cmd/object-storage/enable/enable_test.go @@ -5,29 +5,26 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag -var regionFlag = globalflags.RegionFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &objectstorage.APIClient{} +var testClient = &objectstorage.APIClient{DefaultAPI: &objectstorage.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - regionFlag: testRegion, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -50,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *objectstorage.ApiEnableServiceRequest)) objectstorage.ApiEnableServiceRequest { - request := testClient.EnableService(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.EnableService(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +57,7 @@ func fixtureRequest(mods ...func(request *objectstorage.ApiEnableServiceRequest) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +76,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -100,46 +98,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -162,7 +121,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, objectstorage.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/object-storage/object_storage.go b/internal/cmd/object-storage/object_storage.go index 2adfb7001..1f4c11799 100644 --- a/internal/cmd/object-storage/object_storage.go +++ b/internal/cmd/object-storage/object_storage.go @@ -2,18 +2,19 @@ package objectstorage import ( "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/bucket" + complianceLock "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/compliance-lock" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials" credentialsGroup "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/credentials-group" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/disable" "github.com/stackitcloud/stackit-cli/internal/cmd/object-storage/enable" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "object-storage", Short: "Provides functionality for Object Storage", @@ -25,10 +26,11 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(bucket.NewCmd(params)) cmd.AddCommand(disable.NewCmd(params)) cmd.AddCommand(enable.NewCmd(params)) cmd.AddCommand(credentialsGroup.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) + cmd.AddCommand(complianceLock.NewCmd(params)) } diff --git a/internal/cmd/observability/credentials/create/create.go b/internal/cmd/observability/credentials/create/create.go index 139ed39dd..d3cd35864 100644 --- a/internal/cmd/observability/credentials/create/create.go +++ b/internal/cmd/observability/credentials/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for an Observability instance.", @@ -43,9 +42,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for Observability instance with ID "xxx"`, "$ stackit observability credentials create --instance-id xxx"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -56,22 +55,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -106,7 +103,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiCreateCredentialsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiCreateCredentialsRequest { req := apiClient.CreateCredentials(ctx, model.InstanceId, model.ProjectId) return req } @@ -116,35 +113,16 @@ func outputResult(p *print.Printer, outputFormat, instanceLabel string, resp *ob return fmt.Errorf("response is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { p.Outputf("Created credentials for instance %q.\n\n", instanceLabel) - if resp.Credentials != nil { - // The username field cannot be set by the user, so we only display it if it's not returned empty - username := *resp.Credentials.Username - if username != "" { - p.Outputf("Username: %s\n", username) - } - - p.Outputf("Password: %s\n", utils.PtrString(resp.Credentials.Password)) + // The username field cannot be set by the user, so we only display it if it's not returned empty + username := resp.Credentials.Username + if username != "" { + p.Outputf("Username: %s\n", username) } + + p.Outputf("Password: %s\n", resp.Credentials.Password) return nil - } + }) } diff --git a/internal/cmd/observability/credentials/create/create_test.go b/internal/cmd/observability/credentials/create/create_test.go index 16fcc439d..7f132f9ef 100644 --- a/internal/cmd/observability/credentials/create/create_test.go +++ b/internal/cmd/observability/credentials/create/create_test.go @@ -4,10 +4,11 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -19,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -49,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiCreateCredentialsRequest)) observability.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -59,6 +60,7 @@ func fixtureRequest(mods ...func(request *observability.ApiCreateCredentialsRequ func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,45 +122,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -178,10 +142,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -218,17 +182,16 @@ func TestOutputResult(t *testing.T) { name: "set response with credentials", args: args{ resp: &observability.CreateCredentialsResponse{ - Credentials: observability.NewCredentials("dummy-pw", "dummy-user"), + Credentials: *observability.NewCredentials("dummy-pw", "dummy-user"), }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/credentials/credentials.go b/internal/cmd/observability/credentials/credentials.go index c4aa1e396..2c40cc3d2 100644 --- a/internal/cmd/observability/credentials/credentials.go +++ b/internal/cmd/observability/credentials/credentials.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/observability/credentials/create" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/credentials/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for Observability credentials", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) diff --git a/internal/cmd/observability/credentials/delete/delete.go b/internal/cmd/observability/credentials/delete/delete.go index eeb00d2b9..c2e04ae3c 100644 --- a/internal/cmd/observability/credentials/delete/delete.go +++ b/internal/cmd/observability/credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -30,7 +31,7 @@ type inputModel struct { Username string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", usernameArg), Short: "Deletes credentials of an Observability instance", @@ -54,22 +55,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials for username %q of instance %q? (This cannot be undone)", model.Username, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials for username %q of instance %q? (This cannot be undone)", model.Username, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) _, err = req.Execute() if err != nil { return fmt.Errorf("delete Observability credentials: %w", err) @@ -105,7 +104,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiDeleteCredentialsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiDeleteCredentialsRequest { req := apiClient.DeleteCredentials(ctx, model.InstanceId, model.ProjectId, model.Username) return req } diff --git a/internal/cmd/observability/credentials/delete/delete_test.go b/internal/cmd/observability/credentials/delete/delete_test.go index 21965e75c..d1a52f216 100644 --- a/internal/cmd/observability/credentials/delete/delete_test.go +++ b/internal/cmd/observability/credentials/delete/delete_test.go @@ -9,7 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -17,7 +17,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -60,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiDeleteCredentialsRequest)) observability.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testInstanceId, testProjectId, testUsername) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testInstanceId, testProjectId, testUsername) for _, mod := range mods { mod(&request) } @@ -224,10 +224,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/observability/credentials/list/list.go b/internal/cmd/observability/credentials/list/list.go index d5cc513fe..96d6129cd 100644 --- a/internal/cmd/observability/credentials/list/list.go +++ b/internal/cmd/observability/credentials/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,10 +15,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -33,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists the usernames of all credentials for an Observability instance", @@ -50,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List the usernames of up to 10 credentials for an Observability instance`, "$ stackit observability credentials list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,27 +62,25 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("list Observability credentials: %w", err) } - credentials := *resp.Credentials - if len(credentials) == 0 { - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId - } - params.Printer.Info("No credentials found for instance %q\n", instanceLabel) - return nil + + credentials := resp.GetCredentials() + + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, credentials) }, } configureFlags(cmd) @@ -99,7 +95,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -120,35 +116,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiListCredentialsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiListCredentialsRequest { req := apiClient.ListCredentials(ctx, model.InstanceId, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []observability.ServiceKeysList) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, credentials []observability.ServiceKeysList) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("USERNAME") for i := range credentials { c := credentials[i] - table.AddRow(utils.PtrString(c.Name)) + table.AddRow(c.Name) } err := table.Display(p) if err != nil { @@ -156,5 +139,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials []observabi } return nil - } + }) } diff --git a/internal/cmd/observability/credentials/list/list_test.go b/internal/cmd/observability/credentials/list/list_test.go index 64d92b12f..01e93cc32 100644 --- a/internal/cmd/observability/credentials/list/list_test.go +++ b/internal/cmd/observability/credentials/list/list_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -52,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiListCredentialsRequest)) observability.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.ListCredentials(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -62,6 +62,7 @@ func fixtureRequest(mods ...func(request *observability.ApiListCredentialsReques func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -137,45 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -195,10 +158,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -210,8 +173,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - credentials []observability.ServiceKeysList + outputFormat string + instanceLabel string + credentials []observability.ServiceKeysList } tests := []struct { name string @@ -238,11 +202,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/grafana/describe/describe.go b/internal/cmd/observability/grafana/describe/describe.go index 2ad9103e6..e4e558b35 100644 --- a/internal/cmd/observability/grafana/describe/describe.go +++ b/internal/cmd/observability/grafana/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,11 +17,12 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( - instanceIdArg = "INSTANCE_ID" + instanceIdArg = "INSTANCE_ID" + // Deprecated: showPasswordFlag is deprecated and will be removed on 2026-07-05. showPasswordFlag = "show-password" ) @@ -32,23 +32,19 @@ type inputModel struct { ShowPassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of the Grafana configuration of an Observability instance", - Long: fmt.Sprintf("%s\n%s\n%s", + Long: fmt.Sprintf("%s\n%s", "Shows details of the Grafana configuration of an Observability instance.", `The Grafana dashboard URL and initial credentials (admin user and password) will be shown in the "pretty" output format. These credentials are only valid for first login. Please change the password after first login. After changing, the initial password is no longer valid.`, - `The initial password is hidden by default, if you want to show it use the "--show-password" flag.`, ), Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( examples.NewExample( `Get details of the Grafana configuration of an Observability instance with ID "xxx"`, "$ stackit observability grafana describe xxx"), - examples.NewExample( - `Get details of the Grafana configuration of an Observability instance with ID "xxx" and show the initial admin password`, - "$ stackit observability grafana describe xxx --show-password"), examples.NewExample( `Get details of the Grafana configuration of an Observability instance with ID "xxx" in JSON format`, "$ stackit observability grafana describe xxx --output-format json"), @@ -67,12 +63,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - grafanaConfigsReq := buildGetGrafanaConfigRequest(ctx, model, apiClient) + grafanaConfigsReq := buildGetGrafanaConfigRequest(ctx, model, apiClient.DefaultAPI) grafanaConfigsResp, err := grafanaConfigsReq.Execute() if err != nil { return fmt.Errorf("get Grafana configs: %w", err) } - instanceReq := buildGetInstanceRequest(ctx, model, apiClient) + instanceReq := buildGetInstanceRequest(ctx, model, apiClient.DefaultAPI) instanceResp, err := instanceReq.Execute() if err != nil { return fmt.Errorf("get instance: %w", err) @@ -87,6 +83,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().BoolP(showPasswordFlag, "s", false, "Show password in output") + cobra.CheckErr(cmd.Flags().MarkDeprecated(showPasswordFlag, "This flag is deprecated and will be removed on 2026-07-05.")) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -103,65 +100,43 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ShowPassword: flags.FlagToBoolValue(p, cmd, showPasswordFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildGetGrafanaConfigRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiGetGrafanaConfigsRequest { +func buildGetGrafanaConfigRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiGetGrafanaConfigsRequest { req := apiClient.GetGrafanaConfigs(ctx, model.InstanceId, model.ProjectId) return req } -func buildGetInstanceRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiGetInstanceRequest { +func buildGetInstanceRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiGetInstanceRequest { req := apiClient.GetInstance(ctx, model.InstanceId, model.ProjectId) return req } func outputResult(p *print.Printer, outputFormat string, showPassword bool, grafanaConfigs *observability.GrafanaConfigs, instance *observability.GetInstanceResponse) error { - if instance == nil || instance.Instance == nil { + if instance == nil { return fmt.Errorf("instance or instance content is nil") } else if grafanaConfigs == nil { return fmt.Errorf("grafanaConfigs is nil") } + p.Warn("GrafanaAdminPassword and GrafanaAdminUser are deprecated and will be removed on 2026-07-05.") - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(grafanaConfigs, "", " ") - if err != nil { - return fmt.Errorf("marshal Grafana configs: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(grafanaConfigs, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Grafana configs: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, grafanaConfigs, func() error { + //nolint:staticcheck // field is deprecated but still supported until 2026-07-05 initialAdminPassword := utils.PtrString(instance.Instance.GrafanaAdminPassword) if !showPassword { initialAdminPassword = "" } table := tables.NewTable() - table.AddRow("GRAFANA DASHBOARD", utils.PtrString(instance.Instance.GrafanaUrl)) + table.AddRow("GRAFANA DASHBOARD", instance.Instance.GrafanaUrl) table.AddSeparator() table.AddRow("PUBLIC READ ACCESS", utils.PtrString(grafanaConfigs.PublicReadAccess)) table.AddSeparator() table.AddRow("SINGLE SIGN-ON", utils.PtrString(grafanaConfigs.UseStackitSso)) table.AddSeparator() + //nolint:staticcheck // field is deprecated but still supported until 2026-07-05 table.AddRow("INITIAL ADMIN USER (DEFAULT)", utils.PtrString(instance.Instance.GrafanaAdminUser)) table.AddSeparator() table.AddRow("INITIAL ADMIN PASSWORD (DEFAULT)", initialAdminPassword) @@ -171,5 +146,5 @@ func outputResult(p *print.Printer, outputFormat string, showPassword bool, graf } return nil - } + }) } diff --git a/internal/cmd/observability/grafana/describe/describe_test.go b/internal/cmd/observability/grafana/describe/describe_test.go index d84af41cb..f9bfad81e 100644 --- a/internal/cmd/observability/grafana/describe/describe_test.go +++ b/internal/cmd/observability/grafana/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureGetGrafanaConfigsRequest(mods ...func(request *observability.ApiGetGrafanaConfigsRequest)) observability.ApiGetGrafanaConfigsRequest { - request := testClient.GetGrafanaConfigs(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.GetGrafanaConfigs(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -66,7 +66,7 @@ func fixtureGetGrafanaConfigsRequest(mods ...func(request *observability.ApiGetG } func fixtureGetInstanceRequest(mods ...func(request *observability.ApiGetInstanceRequest)) observability.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.GetInstance(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -169,8 +169,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -202,7 +202,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { @@ -237,10 +237,10 @@ func TestBuildGetGrafanaConfigsRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildGetGrafanaConfigRequest(testCtx, tt.model, testClient) + request := buildGetGrafanaConfigRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -265,10 +265,10 @@ func TestBuildGetInstanceRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildGetInstanceRequest(testCtx, tt.model, testClient) + request := buildGetInstanceRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -306,7 +306,7 @@ func TestOutputResult(t *testing.T) { name: "set instance but no grafana config", args: args{ instance: &observability.GetInstanceResponse{ - Instance: &observability.InstanceSensitiveData{}, + Instance: observability.InstanceSensitiveData{}, }, }, wantErr: true, @@ -316,17 +316,16 @@ func TestOutputResult(t *testing.T) { args: args{ grafanaConfig: &observability.GrafanaConfigs{}, instance: &observability.GetInstanceResponse{ - Instance: &observability.InstanceSensitiveData{}, + Instance: observability.InstanceSensitiveData{}, }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.showPassword, tt.args.grafanaConfig, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.showPassword, tt.args.grafanaConfig, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/grafana/grafana.go b/internal/cmd/observability/grafana/grafana.go index 000da5eac..7ba2a996f 100644 --- a/internal/cmd/observability/grafana/grafana.go +++ b/internal/cmd/observability/grafana/grafana.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/describe" publicreadaccess "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/public-read-access" singlesignon "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/single-sign-on" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "grafana", Short: "Provides functionality for the Grafana configuration of Observability instances", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(publicreadaccess.NewCmd(params)) cmd.AddCommand(singlesignon.NewCmd(params)) diff --git a/internal/cmd/observability/grafana/public-read-access/disable/disable.go b/internal/cmd/observability/grafana/public-read-access/disable/disable.go index 5696bf623..bbc584ef6 100644 --- a/internal/cmd/observability/grafana/public-read-access/disable/disable.go +++ b/internal/cmd/observability/grafana/public-read-access/disable/disable.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" "github.com/spf13/cobra" ) @@ -27,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("disable %s", instanceIdArg), Short: "Disables public read access for Grafana on Observability instances", @@ -54,21 +56,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil || instanceLabel == "" { instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disable Grafana public read access for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disable Grafana public read access for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("build request: %w", err) } @@ -97,19 +97,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient observabilityUtils.ObservabilityClient) (observability.ApiUpdateGrafanaConfigsRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) (observability.ApiUpdateGrafanaConfigsRequest, error) { req := apiClient.UpdateGrafanaConfigs(ctx, model.InstanceId, model.ProjectId) payload, err := observabilityUtils.GetPartialUpdateGrafanaConfigsPayload(ctx, apiClient, model.InstanceId, model.ProjectId, nil, utils.Ptr(false)) if err != nil { diff --git a/internal/cmd/observability/grafana/public-read-access/disable/disable_test.go b/internal/cmd/observability/grafana/public-read-access/disable/disable_test.go index 9dc5019d2..2a8c3b53c 100644 --- a/internal/cmd/observability/grafana/public-read-access/disable/disable_test.go +++ b/internal/cmd/observability/grafana/public-read-access/disable/disable_test.go @@ -5,16 +5,16 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -22,7 +22,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -31,19 +31,15 @@ type observabilityClientMocked struct { getGrafanaConfigsResp *observability.GrafanaConfigs } -func (c *observabilityClientMocked) GetInstanceExecute(ctx context.Context, instanceId, projectId string) (*observability.GetInstanceResponse, error) { - return testClient.GetInstanceExecute(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) UpdateGrafanaConfigs(ctx context.Context, instanceId, projectId string) observability.ApiUpdateGrafanaConfigsRequest { - return testClient.UpdateGrafanaConfigs(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) GetGrafanaConfigsExecute(_ context.Context, _, _ string) (*observability.GrafanaConfigs, error) { - if c.getGrafanaConfigsFails { - return nil, fmt.Errorf("get payload failed") +func (c *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + GetGrafanaConfigsExecuteMock: utils.Ptr(func(_ observability.ApiGetGrafanaConfigsRequest) (*observability.GrafanaConfigs, error) { + if c.getGrafanaConfigsFails { + return nil, fmt.Errorf("get payload failed") + } + return c.getGrafanaConfigsResp, nil + }), } - return c.getGrafanaConfigsResp, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -82,17 +78,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureGrafanaConfigs(mods ...func(gc *observability.GrafanaConfigs)) *observability.GrafanaConfigs { gc := observability.GrafanaConfigs{ - GenericOauth: &observability.GrafanaOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + GenericOauth: &observability.GrafanaOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, PublicReadAccess: utils.Ptr(false), @@ -117,7 +113,7 @@ func fixturePayload(mods ...func(payload *observability.UpdateGrafanaConfigsPayl } func fixtureRequest(mods ...func(request *observability.ApiUpdateGrafanaConfigsRequest)) observability.ApiUpdateGrafanaConfigsRequest { - request := testClient.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) request = request.UpdateGrafanaConfigsPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -186,54 +182,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -262,7 +211,7 @@ func TestBuildRequest(t *testing.T) { }), isValid: true, expectedRequest: fixtureRequest(func(request *observability.ApiUpdateGrafanaConfigsRequest) { - *request = (*request).UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { + *request = request.UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { payload.GenericOauth = nil })) }), @@ -287,7 +236,7 @@ func TestBuildRequest(t *testing.T) { getGrafanaConfigsFails: tt.getGrafanaConfigsFails, getGrafanaConfigsResp: tt.getGrafanaConfigsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, client.newMock()) if err != nil { if !tt.isValid { return @@ -298,6 +247,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/observability/grafana/public-read-access/enable/enable.go b/internal/cmd/observability/grafana/public-read-access/enable/enable.go index 7cd00b9e2..934202d1f 100644 --- a/internal/cmd/observability/grafana/public-read-access/enable/enable.go +++ b/internal/cmd/observability/grafana/public-read-access/enable/enable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) const ( @@ -27,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("enable %s", instanceIdArg), Short: "Enables public read access for Grafana on Observability instances", @@ -54,21 +56,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil || instanceLabel == "" { instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to enable Grafana public read access for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to enable Grafana public read access for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("build request: %w", err) } @@ -97,19 +97,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient observabilityUtils.ObservabilityClient) (observability.ApiUpdateGrafanaConfigsRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) (observability.ApiUpdateGrafanaConfigsRequest, error) { req := apiClient.UpdateGrafanaConfigs(ctx, model.InstanceId, model.ProjectId) payload, err := observabilityUtils.GetPartialUpdateGrafanaConfigsPayload(ctx, apiClient, model.InstanceId, model.ProjectId, nil, utils.Ptr(true)) if err != nil { diff --git a/internal/cmd/observability/grafana/public-read-access/enable/enable_test.go b/internal/cmd/observability/grafana/public-read-access/enable/enable_test.go index 6d1fbea0a..5a7c9c4cb 100644 --- a/internal/cmd/observability/grafana/public-read-access/enable/enable_test.go +++ b/internal/cmd/observability/grafana/public-read-access/enable/enable_test.go @@ -5,16 +5,16 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -22,7 +22,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -31,19 +31,15 @@ type observabilityClientMocked struct { getGrafanaConfigsResp *observability.GrafanaConfigs } -func (c *observabilityClientMocked) GetInstanceExecute(ctx context.Context, instanceId, projectId string) (*observability.GetInstanceResponse, error) { - return testClient.GetInstanceExecute(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) UpdateGrafanaConfigs(ctx context.Context, instanceId, projectId string) observability.ApiUpdateGrafanaConfigsRequest { - return testClient.UpdateGrafanaConfigs(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) GetGrafanaConfigsExecute(_ context.Context, _, _ string) (*observability.GrafanaConfigs, error) { - if c.getGrafanaConfigsFails { - return nil, fmt.Errorf("get payload failed") +func (c *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + GetGrafanaConfigsExecuteMock: utils.Ptr(func(_ observability.ApiGetGrafanaConfigsRequest) (*observability.GrafanaConfigs, error) { + if c.getGrafanaConfigsFails { + return nil, fmt.Errorf("get payload failed") + } + return c.getGrafanaConfigsResp, nil + }), } - return c.getGrafanaConfigsResp, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -82,17 +78,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureGrafanaConfigs(mods ...func(gc *observability.GrafanaConfigs)) *observability.GrafanaConfigs { gc := observability.GrafanaConfigs{ - GenericOauth: &observability.GrafanaOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + GenericOauth: &observability.GrafanaOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, PublicReadAccess: utils.Ptr(false), @@ -117,7 +113,7 @@ func fixturePayload(mods ...func(payload *observability.UpdateGrafanaConfigsPayl } func fixtureRequest(mods ...func(request *observability.ApiUpdateGrafanaConfigsRequest)) observability.ApiUpdateGrafanaConfigsRequest { - request := testClient.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) request = request.UpdateGrafanaConfigsPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -186,54 +182,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -262,7 +211,7 @@ func TestBuildRequest(t *testing.T) { }), isValid: true, expectedRequest: fixtureRequest(func(request *observability.ApiUpdateGrafanaConfigsRequest) { - *request = (*request).UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { + *request = request.UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { payload.GenericOauth = nil })) }), @@ -287,7 +236,7 @@ func TestBuildRequest(t *testing.T) { getGrafanaConfigsFails: tt.getGrafanaConfigsFails, getGrafanaConfigsResp: tt.getGrafanaConfigsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, client.newMock()) if err != nil { if !tt.isValid { return @@ -298,6 +247,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/observability/grafana/public-read-access/public_read_access.go b/internal/cmd/observability/grafana/public-read-access/public_read_access.go index 94c27eb36..bf45ec5df 100644 --- a/internal/cmd/observability/grafana/public-read-access/public_read_access.go +++ b/internal/cmd/observability/grafana/public-read-access/public_read_access.go @@ -3,16 +3,17 @@ package publicreadaccess import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/public-read-access/disable" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/public-read-access/enable" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "public-read-access", Short: "Enable or disable public read access for Grafana in Observability instances", @@ -27,7 +28,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(enable.NewCmd(params)) cmd.AddCommand(disable.NewCmd(params)) } diff --git a/internal/cmd/observability/grafana/single-sign-on/disable/disable.go b/internal/cmd/observability/grafana/single-sign-on/disable/disable.go index bf6de05a4..4dc229409 100644 --- a/internal/cmd/observability/grafana/single-sign-on/disable/disable.go +++ b/internal/cmd/observability/grafana/single-sign-on/disable/disable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) const ( @@ -27,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("disable %s", instanceIdArg), Short: "Disables single sign-on for Grafana on Observability instances", @@ -54,21 +56,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil || instanceLabel == "" { instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disable single sign-on for Grafana for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disable single sign-on for Grafana for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("build request: %w", err) } @@ -97,19 +97,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient observabilityUtils.ObservabilityClient) (observability.ApiUpdateGrafanaConfigsRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) (observability.ApiUpdateGrafanaConfigsRequest, error) { req := apiClient.UpdateGrafanaConfigs(ctx, model.InstanceId, model.ProjectId) payload, err := observabilityUtils.GetPartialUpdateGrafanaConfigsPayload(ctx, apiClient, model.InstanceId, model.ProjectId, utils.Ptr(false), nil) if err != nil { diff --git a/internal/cmd/observability/grafana/single-sign-on/disable/disable_test.go b/internal/cmd/observability/grafana/single-sign-on/disable/disable_test.go index e536ab098..3de0d5183 100644 --- a/internal/cmd/observability/grafana/single-sign-on/disable/disable_test.go +++ b/internal/cmd/observability/grafana/single-sign-on/disable/disable_test.go @@ -5,16 +5,16 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -22,7 +22,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -31,19 +31,15 @@ type observabilityClientMocked struct { getGrafanaConfigsResp *observability.GrafanaConfigs } -func (c *observabilityClientMocked) GetInstanceExecute(ctx context.Context, instanceId, projectId string) (*observability.GetInstanceResponse, error) { - return testClient.GetInstanceExecute(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) UpdateGrafanaConfigs(ctx context.Context, instanceId, projectId string) observability.ApiUpdateGrafanaConfigsRequest { - return testClient.UpdateGrafanaConfigs(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) GetGrafanaConfigsExecute(_ context.Context, _, _ string) (*observability.GrafanaConfigs, error) { - if c.getGrafanaConfigsFails { - return nil, fmt.Errorf("get payload failed") +func (c *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + GetGrafanaConfigsExecuteMock: utils.Ptr(func(_ observability.ApiGetGrafanaConfigsRequest) (*observability.GrafanaConfigs, error) { + if c.getGrafanaConfigsFails { + return nil, fmt.Errorf("get payload failed") + } + return c.getGrafanaConfigsResp, nil + }), } - return c.getGrafanaConfigsResp, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -82,17 +78,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureGrafanaConfigs(mods ...func(gc *observability.GrafanaConfigs)) *observability.GrafanaConfigs { gc := observability.GrafanaConfigs{ - GenericOauth: &observability.GrafanaOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + GenericOauth: &observability.GrafanaOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, PublicReadAccess: utils.Ptr(false), @@ -117,7 +113,7 @@ func fixturePayload(mods ...func(payload *observability.UpdateGrafanaConfigsPayl } func fixtureRequest(mods ...func(request *observability.ApiUpdateGrafanaConfigsRequest)) observability.ApiUpdateGrafanaConfigsRequest { - request := testClient.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) request = request.UpdateGrafanaConfigsPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -186,54 +182,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -262,7 +211,7 @@ func TestBuildRequest(t *testing.T) { }), isValid: true, expectedRequest: fixtureRequest(func(request *observability.ApiUpdateGrafanaConfigsRequest) { - *request = (*request).UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { + *request = request.UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { payload.GenericOauth = nil })) }), @@ -287,7 +236,7 @@ func TestBuildRequest(t *testing.T) { getGrafanaConfigsFails: tt.getGrafanaConfigsFails, getGrafanaConfigsResp: tt.getGrafanaConfigsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, client.newMock()) if err != nil { if !tt.isValid { return @@ -298,6 +247,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/observability/grafana/single-sign-on/enable/enable.go b/internal/cmd/observability/grafana/single-sign-on/enable/enable.go index 84b2bc0c4..a7a8213b6 100644 --- a/internal/cmd/observability/grafana/single-sign-on/enable/enable.go +++ b/internal/cmd/observability/grafana/single-sign-on/enable/enable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("enable %s", instanceIdArg), Short: "Enables single sign-on for Grafana on Observability instances", @@ -54,21 +55,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil || instanceLabel == "" { instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to enable single sign-on for Grafana for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to enable single sign-on for Grafana for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("build request: %w", err) } @@ -97,19 +96,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient observabilityUtils.ObservabilityClient) (observability.ApiUpdateGrafanaConfigsRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) (observability.ApiUpdateGrafanaConfigsRequest, error) { req := apiClient.UpdateGrafanaConfigs(ctx, model.InstanceId, model.ProjectId) payload, err := observabilityUtils.GetPartialUpdateGrafanaConfigsPayload(ctx, apiClient, model.InstanceId, model.ProjectId, utils.Ptr(true), nil) if err != nil { diff --git a/internal/cmd/observability/grafana/single-sign-on/enable/enable_test.go b/internal/cmd/observability/grafana/single-sign-on/enable/enable_test.go index 033227257..915445ae4 100644 --- a/internal/cmd/observability/grafana/single-sign-on/enable/enable_test.go +++ b/internal/cmd/observability/grafana/single-sign-on/enable/enable_test.go @@ -5,16 +5,15 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -22,7 +21,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -31,19 +30,15 @@ type observabilityClientMocked struct { getGrafanaConfigsResp *observability.GrafanaConfigs } -func (c *observabilityClientMocked) GetInstanceExecute(ctx context.Context, instanceId, projectId string) (*observability.GetInstanceResponse, error) { - return testClient.GetInstanceExecute(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) UpdateGrafanaConfigs(ctx context.Context, instanceId, projectId string) observability.ApiUpdateGrafanaConfigsRequest { - return testClient.UpdateGrafanaConfigs(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) GetGrafanaConfigsExecute(_ context.Context, _, _ string) (*observability.GrafanaConfigs, error) { - if c.getGrafanaConfigsFails { - return nil, fmt.Errorf("get payload failed") +func (c *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + GetGrafanaConfigsExecuteMock: utils.Ptr(func(_ observability.ApiGetGrafanaConfigsRequest) (*observability.GrafanaConfigs, error) { + if c.getGrafanaConfigsFails { + return nil, fmt.Errorf("get payload failed") + } + return c.getGrafanaConfigsResp, nil + }), } - return c.getGrafanaConfigsResp, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -82,17 +77,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixtureGrafanaConfigs(mods ...func(gc *observability.GrafanaConfigs)) *observability.GrafanaConfigs { gc := observability.GrafanaConfigs{ - GenericOauth: &observability.GrafanaOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + GenericOauth: &observability.GrafanaOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, PublicReadAccess: utils.Ptr(false), @@ -117,7 +112,7 @@ func fixturePayload(mods ...func(payload *observability.UpdateGrafanaConfigsPayl } func fixtureRequest(mods ...func(request *observability.ApiUpdateGrafanaConfigsRequest)) observability.ApiUpdateGrafanaConfigsRequest { - request := testClient.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.UpdateGrafanaConfigs(testCtx, testInstanceId, testProjectId) request = request.UpdateGrafanaConfigsPayload(*fixturePayload()) for _, mod := range mods { mod(&request) @@ -186,54 +181,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -262,7 +210,7 @@ func TestBuildRequest(t *testing.T) { }), isValid: true, expectedRequest: fixtureRequest(func(request *observability.ApiUpdateGrafanaConfigsRequest) { - *request = (*request).UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { + *request = request.UpdateGrafanaConfigsPayload(*fixturePayload(func(payload *observability.UpdateGrafanaConfigsPayload) { payload.GenericOauth = nil })) }), @@ -287,7 +235,7 @@ func TestBuildRequest(t *testing.T) { getGrafanaConfigsFails: tt.getGrafanaConfigsFails, getGrafanaConfigsResp: tt.getGrafanaConfigsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, client.newMock()) if err != nil { if !tt.isValid { return @@ -298,6 +246,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/observability/grafana/single-sign-on/single_sign_on.go b/internal/cmd/observability/grafana/single-sign-on/single_sign_on.go index c4a73ada2..293066b8f 100644 --- a/internal/cmd/observability/grafana/single-sign-on/single_sign_on.go +++ b/internal/cmd/observability/grafana/single-sign-on/single_sign_on.go @@ -3,16 +3,17 @@ package singlesignon import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/single-sign-on/disable" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/grafana/single-sign-on/enable" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "single-sign-on", Aliases: []string{"sso"}, @@ -28,7 +29,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(enable.NewCmd(params)) cmd.AddCommand(disable.NewCmd(params)) } diff --git a/internal/cmd/observability/instance/create/create.go b/internal/cmd/observability/instance/create/create.go index 37f3a8114..12229c1f6 100644 --- a/internal/cmd/observability/instance/create/create.go +++ b/internal/cmd/observability/instance/create/create.go @@ -2,12 +2,11 @@ package create import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,12 +16,12 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api/wait" + observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" ) const ( @@ -39,7 +38,7 @@ type inputModel struct { PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates an Observability instance", @@ -53,9 +52,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create an Observability instance with name "my-instance" and specify plan by ID`, "$ stackit observability instance create --name my-instance --plan-id xxx"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,16 +71,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create an Observability instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create an Observability instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var observabilityInvalidPlanError *cliErr.ObservabilityInvalidPlanError if !errors.As(err, &observabilityInvalidPlanError) { @@ -93,17 +90,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create Observability instance: %w", err) } - instanceId := *resp.InstanceId + instanceId := resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, instanceId, model.ProjectId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, instanceId, model.ProjectId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Observability instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) @@ -122,7 +119,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -149,30 +146,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { PlanName: planName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type observabilityClient interface { - CreateInstance(ctx context.Context, projectId string) observability.ApiCreateInstanceRequest - ListPlansExecute(ctx context.Context, projectId string) (*observability.PlansResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient observabilityClient) (observability.ApiCreateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) (observability.ApiCreateInstanceRequest, error) { req := apiClient.CreateInstance(ctx, model.ProjectId) - var planId *string + var planId string var err error - plans, err := apiClient.ListPlansExecute(ctx, model.ProjectId) + plans, err := apiClient.ListPlans(ctx, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get Observability plans: %w", err) } @@ -191,7 +175,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient observabilit if err != nil { return req, err } - planId = model.PlanId + planId = *model.PlanId } req = req.CreateInstancePayload(observability.CreateInstancePayload{ @@ -206,29 +190,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe return fmt.Errorf("resp is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } - p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, utils.PtrString(resp.InstanceId)) + p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, resp.InstanceId) return nil - } + }) } diff --git a/internal/cmd/observability/instance/create/create_test.go b/internal/cmd/observability/instance/create/create_test.go index 6cdeda809..a64925569 100644 --- a/internal/cmd/observability/instance/create/create_test.go +++ b/internal/cmd/observability/instance/create/create_test.go @@ -5,15 +5,15 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,22 +21,22 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} type observabilityClientMocked struct { returnError bool listPlansResponse *observability.PlansResponse } -func (c *observabilityClientMocked) CreateInstance(ctx context.Context, projectId string) observability.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId) -} - -func (c *observabilityClientMocked) ListPlansExecute(_ context.Context, _ string) (*observability.PlansResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list plans failed") +func (c *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + ListPlansExecuteMock: utils.Ptr(func(_ observability.ApiListPlansRequest) (*observability.PlansResponse, error) { + if c.returnError { + return nil, fmt.Errorf("list plans failed") + } + return c.listPlansResponse, nil + }), } - return c.listPlansResponse, nil } var testProjectId = uuid.NewString() @@ -70,10 +70,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiCreateInstanceRequest)) observability.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId) request = request.CreateInstancePayload(observability.CreateInstancePayload{ Name: utils.Ptr("example-name"), - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }) for _, mod := range mods { mod(&request) @@ -83,10 +83,10 @@ func fixtureRequest(mods ...func(request *observability.ApiCreateInstanceRequest func fixturePlansResponse(mods ...func(response *observability.PlansResponse)) *observability.PlansResponse { response := &observability.PlansResponse{ - Plans: &[]observability.Plan{ + Plans: []observability.Plan{ { Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Id: testPlanId, }, }, } @@ -99,6 +99,7 @@ func fixturePlansResponse(mods ...func(response *observability.PlansResponse)) * func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -175,46 +176,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -287,8 +249,8 @@ func TestBuildRequest(t *testing.T) { }, ), getPlansResponse: fixturePlansResponse(), - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(observability.CreateInstancePayload{PlanId: utils.Ptr(testPlanId)}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId). + CreateInstancePayload(observability.CreateInstancePayload{PlanId: testPlanId}), isValid: true, }, { @@ -301,8 +263,8 @@ func TestBuildRequest(t *testing.T) { }, ), getPlansResponse: fixturePlansResponse(), - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(observability.CreateInstancePayload{PlanId: utils.Ptr(testPlanId)}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId). + CreateInstancePayload(observability.CreateInstancePayload{PlanId: testPlanId}), isValid: true, }, } @@ -313,7 +275,7 @@ func TestBuildRequest(t *testing.T) { returnError: tt.getPlansFails, listPlansResponse: tt.getPlansResponse, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, client.newMock()) if err != nil { if !tt.isValid { return @@ -328,6 +290,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -361,11 +326,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/instance/delete/delete.go b/internal/cmd/observability/instance/delete/delete.go index ff0d1bd62..50e399417 100644 --- a/internal/cmd/observability/instance/delete/delete.go +++ b/internal/cmd/observability/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes an Observability instance", @@ -53,22 +54,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) _, err = req.Execute() if err != nil { return fmt.Errorf("delete Observability instance: %w", err) @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.InstanceId, model.ProjectId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Observability instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiDeleteInstanceRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiDeleteInstanceRequest { req := apiClient.DeleteInstance(ctx, model.InstanceId, model.ProjectId) return req } diff --git a/internal/cmd/observability/instance/delete/delete_test.go b/internal/cmd/observability/instance/delete/delete_test.go index d8432900f..be1c0f7ff 100644 --- a/internal/cmd/observability/instance/delete/delete_test.go +++ b/internal/cmd/observability/instance/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiDeleteInstanceRequest)) observability.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -138,54 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -205,10 +157,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/observability/instance/describe/describe.go b/internal/cmd/observability/instance/describe/describe.go index 506bdcbc1..22d6bc6b8 100644 --- a/internal/cmd/observability/instance/describe/describe.go +++ b/internal/cmd/observability/instance/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -29,7 +28,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of an Observability instance", @@ -56,7 +55,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("read Observability instance: %w", err) @@ -81,19 +80,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiGetInstanceRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiGetInstanceRequest { req := apiClient.GetInstance(ctx, model.InstanceId, model.ProjectId) return req } @@ -103,54 +94,34 @@ func outputResult(p *print.Printer, outputFormat string, instance *observability return fmt.Errorf("instance is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(instance.Id)) + table.AddRow("ID", instance.Id) table.AddSeparator() table.AddRow("NAME", utils.PtrString(instance.Name)) table.AddSeparator() - table.AddRow("STATUS", utils.PtrString(instance.Status)) + table.AddRow("STATUS", instance.Status) table.AddSeparator() - table.AddRow("PLAN NAME", utils.PtrString(instance.PlanName)) + table.AddRow("PLAN NAME", instance.PlanName) + table.AddSeparator() + plan := instance.Instance.Plan + table.AddRow("METRIC SAMPLES (PER MIN)", plan.TotalMetricSamples) + table.AddSeparator() + table.AddRow("LOGS (GB)", plan.LogsStorage) + table.AddSeparator() + table.AddRow("TRACES (GB)", plan.TracesStorage) + table.AddSeparator() + table.AddRow("NOTIFICATION RULES", plan.AlertRules) + table.AddSeparator() + table.AddRow("GRAFANA USERS", plan.GrafanaGlobalUsers) + table.AddSeparator() + table.AddRow("GRAFANA URL", instance.Instance.GrafanaUrl) table.AddSeparator() - if inst := instance.Instance; inst != nil { - if plan := inst.Plan; plan != nil { - table.AddRow("METRIC SAMPLES (PER MIN)", utils.PtrString(plan.TotalMetricSamples)) - table.AddSeparator() - table.AddRow("LOGS (GB)", utils.PtrString(plan.LogsStorage)) - table.AddSeparator() - table.AddRow("TRACES (GB)", utils.PtrString(plan.TracesStorage)) - table.AddSeparator() - table.AddRow("NOTIFICATION RULES", utils.PtrString(plan.AlertRules)) - table.AddSeparator() - table.AddRow("GRAFANA USERS", utils.PtrString(plan.GrafanaGlobalUsers)) - table.AddSeparator() - } - table.AddRow("GRAFANA URL", utils.PtrString(inst.GrafanaUrl)) - table.AddSeparator() - } err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/observability/instance/describe/describe_test.go b/internal/cmd/observability/instance/describe/describe_test.go index 62f60d451..853f95d0d 100644 --- a/internal/cmd/observability/instance/describe/describe_test.go +++ b/internal/cmd/observability/instance/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiGetInstanceRequest)) observability.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.GetInstance(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -138,54 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -205,10 +158,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -241,11 +194,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/instance/instance.go b/internal/cmd/observability/instance/instance.go index 47a84edf6..955ae39ec 100644 --- a/internal/cmd/observability/instance/instance.go +++ b/internal/cmd/observability/instance/instance.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/observability/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/instance/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for Observability instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/observability/instance/list/list.go b/internal/cmd/observability/instance/list/list.go index 011842a4e..b77731725 100644 --- a/internal/cmd/observability/instance/list/list.go +++ b/internal/cmd/observability/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Observability instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Observability instances`, "$ stackit observability instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -61,20 +61,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("get Observability instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + + instances := resp.GetInstances() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +80,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +92,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,51 +111,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiListInstancesRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiListInstancesRequest { req := apiClient.ListInstances(ctx, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, instances []observability.ProjectInstanceFull) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []observability.ProjectInstanceFull) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "PLAN", "STATUS") for i := range instances { instance := instances[i] table.AddRow( - utils.PtrString(instance.Id), + instance.Id, utils.PtrString(instance.Name), - utils.PtrString(instance.PlanName), - utils.PtrString(instance.Status), + instance.PlanName, + instance.Status, ) } err := table.Display(p) @@ -166,5 +143,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []observabili } return nil - } + }) } diff --git a/internal/cmd/observability/instance/list/list_test.go b/internal/cmd/observability/instance/list/list_test.go index 33157c100..25a4c416d 100644 --- a/internal/cmd/observability/instance/list/list_test.go +++ b/internal/cmd/observability/instance/list/list_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -50,7 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiListInstancesRequest)) observability.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +59,7 @@ func fixtureRequest(mods ...func(request *observability.ApiListInstancesRequest) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -114,47 +114,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -174,10 +134,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -190,6 +150,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []observability.ProjectInstanceFull } tests := []struct { @@ -217,11 +178,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/instance/update/update.go b/internal/cmd/observability/instance/update/update.go index b314a6ecc..be02114cc 100644 --- a/internal/cmd/observability/instance/update/update.go +++ b/internal/cmd/observability/instance/update/update.go @@ -5,7 +5,8 @@ import ( "errors" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,8 +19,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api/wait" ) const ( @@ -39,7 +40,7 @@ type inputModel struct { PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates an Observability instance", @@ -69,22 +70,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil || instanceLabel == "" { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var observabilityInvalidPlanError *cliErr.ObservabilityInvalidPlanError if !errors.As(err, &observabilityInvalidPlanError) { @@ -101,13 +100,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.UpdateInstanceWaitHandler(ctx, apiClient, instanceId, model.ProjectId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.UpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, instanceId, model.ProjectId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Observability instance update: %w", err) } - s.Stop() } operationState := "Updated" @@ -158,46 +157,32 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceName: instanceName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type observabilityClient interface { - UpdateInstance(ctx context.Context, instanceId, projectId string) observability.ApiUpdateInstanceRequest - ListPlansExecute(ctx context.Context, projectId string) (*observability.PlansResponse, error) - GetInstanceExecute(ctx context.Context, instanceId, projectId string) (*observability.GetInstanceResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient observabilityClient) (observability.ApiUpdateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) (observability.ApiUpdateInstanceRequest, error) { req := apiClient.UpdateInstance(ctx, model.InstanceId, model.ProjectId) var err error - plans, err := apiClient.ListPlansExecute(ctx, model.ProjectId) + plans, err := apiClient.ListPlans(ctx, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get Observability plans: %w", err) } - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.InstanceId, model.ProjectId) + currentInstance, err := apiClient.GetInstance(ctx, model.InstanceId, model.ProjectId).Execute() if err != nil { return req, fmt.Errorf("get Observability instance: %w", err) } payload := observability.UpdateInstancePayload{ - PlanId: currentInstance.PlanId, + PlanId: ¤tInstance.PlanId, Name: currentInstance.Name, } if model.PlanId == nil && model.PlanName != "" { - payload.PlanId, err = observabilityUtils.LoadPlanId(model.PlanName, plans) + planId, err := observabilityUtils.LoadPlanId(model.PlanName, plans) if err != nil { var observabilityInvalidPlanError *cliErr.ObservabilityInvalidPlanError if !errors.As(err, &observabilityInvalidPlanError) { @@ -205,6 +190,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient observabilit } return req, err } + payload.PlanId = &planId } else if model.PlanId != nil && model.PlanName == "" { err := observabilityUtils.ValidatePlanId(*model.PlanId, plans) if err != nil { diff --git a/internal/cmd/observability/instance/update/update_test.go b/internal/cmd/observability/instance/update/update_test.go index fd798093c..74ffc17fe 100644 --- a/internal/cmd/observability/instance/update/update_test.go +++ b/internal/cmd/observability/instance/update/update_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} type observabilityClientMocked struct { listPlansError bool @@ -30,22 +29,21 @@ type observabilityClientMocked struct { getInstanceResponse *observability.GetInstanceResponse } -func (c *observabilityClientMocked) UpdateInstance(ctx context.Context, instanceId, projectId string) observability.ApiUpdateInstanceRequest { - return testClient.UpdateInstance(ctx, instanceId, projectId) -} - -func (c *observabilityClientMocked) ListPlansExecute(_ context.Context, _ string) (*observability.PlansResponse, error) { - if c.listPlansError { - return nil, fmt.Errorf("list flavors failed") - } - return c.listPlansResponse, nil -} - -func (c *observabilityClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*observability.GetInstanceResponse, error) { - if c.getInstanceError { - return nil, fmt.Errorf("get instance failed") +func (c *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + ListPlansExecuteMock: utils.Ptr(func(_ observability.ApiListPlansRequest) (*observability.PlansResponse, error) { + if c.listPlansError { + return nil, fmt.Errorf("list flavors failed") + } + return c.listPlansResponse, nil + }), + GetInstanceExecuteMock: utils.Ptr(func(_ observability.ApiGetInstanceRequest) (*observability.GetInstanceResponse, error) { + if c.getInstanceError { + return nil, fmt.Errorf("get instance failed") + } + return c.getInstanceResponse, nil + }), } - return c.getInstanceResponse, nil } const ( @@ -96,7 +94,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiUpdateInstanceRequest)) observability.ApiUpdateInstanceRequest { - request := testClient.UpdateInstance(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.UpdateInstance(testCtx, testInstanceId, testProjectId) request = request.UpdateInstancePayload(observability.UpdateInstancePayload{ PlanId: utils.Ptr(testNewPlanId), Name: utils.Ptr(testInstanceName), @@ -109,10 +107,10 @@ func fixtureRequest(mods ...func(request *observability.ApiUpdateInstanceRequest func fixturePlansResponse(mods ...func(response *observability.PlansResponse)) *observability.PlansResponse { response := &observability.PlansResponse{ - Plans: &[]observability.Plan{ + Plans: []observability.Plan{ { Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testNewPlanId), + Id: testNewPlanId, }, }, } @@ -124,7 +122,7 @@ func fixturePlansResponse(mods ...func(response *observability.PlansResponse)) * func fixtureGetInstanceResponse(mods ...func(response *observability.GetInstanceResponse)) *observability.GetInstanceResponse { response := &observability.GetInstanceResponse{ - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, Name: utils.Ptr(testInstanceName), } for _, mod := range mods { @@ -240,54 +238,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -423,7 +374,7 @@ func TestBuildRequest(t *testing.T) { getInstanceError: tt.getInstanceFails, getInstanceResponse: tt.getInstanceResponse, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, client.newMock()) if err != nil { if !tt.isValid { return @@ -438,6 +389,9 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/observability/observability.go b/internal/cmd/observability/observability.go index 136f29637..66345691a 100644 --- a/internal/cmd/observability/observability.go +++ b/internal/cmd/observability/observability.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/observability/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/plans" scrapeconfig "github.com/stackitcloud/stackit-cli/internal/cmd/observability/scrape-config" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "observability", Short: "Provides functionality for Observability", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(grafana.NewCmd(params)) cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) diff --git a/internal/cmd/observability/plans/plans.go b/internal/cmd/observability/plans/plans.go index 7fe3feabf..386ee26e0 100644 --- a/internal/cmd/observability/plans/plans.go +++ b/internal/cmd/observability/plans/plans.go @@ -2,10 +2,10 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,9 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists all Observability service plans", @@ -48,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Observability service plans`, "$ stackit observability plans --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -62,20 +61,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("get Observability service plans: %w", err) } - plans := *resp.Plans - if len(plans) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No plans found for project %q\n", projectLabel) - return nil + + plans := resp.GetPlans() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -83,7 +80,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { plans = plans[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, plans) + return outputResult(params.Printer, model.OutputFormat, projectLabel, plans) }, } @@ -95,7 +92,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -114,48 +111,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiListPlansRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiListPlansRequest { req := apiClient.ListPlans(ctx, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, plans []observability.Plan) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("marshal Observability plans: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Observability plans: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []observability.Plan) error { + return p.OutputResult(outputFormat, plans, func() error { + if len(plans) == 0 { + p.Outputf("No plans found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "PLAN NAME", "DESCRIPTION") for i := range plans { o := plans[i] table.AddRow( - utils.PtrString(o.Id), + o.Id, utils.PtrString(o.Name), utils.PtrString(o.Description), ) @@ -168,5 +144,5 @@ func outputResult(p *print.Printer, outputFormat string, plans []observability.P } return nil - } + }) } diff --git a/internal/cmd/observability/plans/plans_test.go b/internal/cmd/observability/plans/plans_test.go index 2228a0402..9c9b28b08 100644 --- a/internal/cmd/observability/plans/plans_test.go +++ b/internal/cmd/observability/plans/plans_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -50,7 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiListPlansRequest)) observability.ApiListPlansRequest { - request := testClient.ListPlans(testCtx, testProjectId) + request := testClient.DefaultAPI.ListPlans(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +59,7 @@ func fixtureRequest(mods ...func(request *observability.ApiListPlansRequest)) ob func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -114,48 +114,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -175,10 +134,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -191,6 +150,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string plans []observability.Plan } tests := []struct { @@ -218,11 +178,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.plans); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/scrape-config/create/create.go b/internal/cmd/observability/scrape-config/create/create.go index d11d4b575..740166324 100644 --- a/internal/cmd/observability/scrape-config/create/create.go +++ b/internal/cmd/observability/scrape-config/create/create.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,11 +16,10 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api/wait" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { Payload *observability.CreateScrapeConfigPayload } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a scrape configuration for an Observability instance", @@ -60,9 +60,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { ``, `$ stackit observability scrape-config create --payload @./payload.json --instance-id xxx`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -73,7 +73,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId @@ -88,16 +88,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { model.Payload = &defaultPayload } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create scrape configuration %q on Observability instance %q?", *model.Payload.JobName, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create scrape configuration %q on Observability instance %q?", model.Payload.JobName, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) _, err = req.Execute() if err != nil { return fmt.Errorf("create scrape configuration: %w", err) @@ -107,20 +105,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating scrape config") - _, err = wait.CreateScrapeConfigWaitHandler(ctx, apiClient, model.InstanceId, *jobName, model.ProjectId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating scrape config", func() error { + _, err = wait.CreateScrapeConfigWaitHandler(ctx, apiClient.DefaultAPI, model.InstanceId, jobName, model.ProjectId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for scrape configuration creation: %w", err) } - s.Stop() } operationState := "Created" if model.Async { operationState = "Triggered creation of" } - params.Printer.Outputf("%s scrape configuration with name %q for Observability instance %q\n", operationState, utils.PtrString(jobName), instanceLabel) + params.Printer.Outputf("%s scrape configuration with name %q for Observability instance %q\n", operationState, jobName, instanceLabel) return nil }, } @@ -136,7 +134,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -159,7 +157,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiCreateScrapeConfigRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiCreateScrapeConfigRequest { req := apiClient.CreateScrapeConfig(ctx, model.InstanceId, model.ProjectId) req = req.CreateScrapeConfigPayload(*model.Payload) diff --git a/internal/cmd/observability/scrape-config/create/create_test.go b/internal/cmd/observability/scrape-config/create/create_test.go index 533bf457b..2431e1ed9 100644 --- a/internal/cmd/observability/scrape-config/create/create_test.go +++ b/internal/cmd/observability/scrape-config/create/create_test.go @@ -5,13 +5,15 @@ import ( "fmt" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +21,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -32,36 +34,36 @@ var testPayload = &observability.CreateScrapeConfigPayload{ HonorLabels: utils.Ptr(true), HonorTimeStamps: utils.Ptr(true), MetricsPath: utils.Ptr("/metrics"), - JobName: utils.Ptr("default-name"), - MetricsRelabelConfigs: &[]observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner{ + JobName: "default-name", + MetricsRelabelConfigs: []observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner{ { Action: observability.CREATESCRAPECONFIGPAYLOADMETRICSRELABELCONFIGSINNERACTION_REPLACE.Ptr(), - Modulus: utils.Ptr(1.0), + Modulus: utils.Ptr(float32(1.0)), Regex: utils.Ptr("regex"), Replacement: utils.Ptr("replacement"), Separator: utils.Ptr("separator"), - SourceLabels: &[]string{"sourceLabel"}, + SourceLabels: []string{"sourceLabel"}, TargetLabel: utils.Ptr("targetLabel"), }, }, - Params: &map[string]interface{}{ + Params: map[string]interface{}{ "key": []interface{}{string("value1"), string("value2")}, "key2": []interface{}{}, }, - SampleLimit: utils.Ptr(1.0), - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTPS.Ptr(), - ScrapeInterval: utils.Ptr("interval"), - ScrapeTimeout: utils.Ptr("timeout"), - StaticConfigs: &[]observability.CreateScrapeConfigPayloadStaticConfigsInner{ + SampleLimit: utils.Ptr(float32(1.0)), + Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTPS, + ScrapeInterval: "interval", + ScrapeTimeout: "timeout", + StaticConfigs: []observability.CreateScrapeConfigPayloadStaticConfigsInner{ { - Labels: &map[string]interface{}{ + Labels: map[string]interface{}{ "label": "value", "label2": "value2", }, - Targets: &[]string{"target"}, + Targets: []string{"target"}, }, }, - TlsConfig: &observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig{ + TlsConfig: &observability.CreateScrapeConfigPayloadTlsConfig{ InsecureSkipVerify: utils.Ptr(true), }, } @@ -135,7 +137,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiCreateScrapeConfigRequest)) observability.ApiCreateScrapeConfigRequest { - request := testClient.CreateScrapeConfig(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.CreateScrapeConfig(testCtx, testInstanceId, testProjectId) request = request.CreateScrapeConfigPayload(*testPayload) for _, mod := range mods { mod(&request) @@ -146,6 +148,7 @@ func fixtureRequest(mods ...func(request *observability.ApiCreateScrapeConfigReq func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -230,55 +233,12 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(*model, *tt.expectedModel, - cmpopts.EquateComparable(testCtx), - ) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithOptions(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, nil, tt.isValid, []testutils.TestingOption{ + testutils.WithCmpOptions(cmp.FilterPath(func(p cmp.Path) bool { + last := p.Last().String() + return last == ".AdditionalProperties" + }, cmp.Ignore())), + }) }) } } @@ -299,10 +259,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/observability/scrape-config/delete/delete.go b/internal/cmd/observability/scrape-config/delete/delete.go index 5ec7e3931..c953c4e5e 100644 --- a/internal/cmd/observability/scrape-config/delete/delete.go +++ b/internal/cmd/observability/scrape-config/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api/wait" ) const ( @@ -32,7 +33,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", jobNameArg), Short: "Deletes a scrape configuration from an Observability instance", @@ -56,22 +57,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete scrape configuration %q on Observability instance %q? (This cannot be undone)", model.JobName, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete scrape configuration %q on Observability instance %q? (This cannot be undone)", model.JobName, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) _, err = req.Execute() if err != nil { return fmt.Errorf("delete scrape configuration: %w", err) @@ -79,13 +78,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting scrape config") - _, err = wait.DeleteScrapeConfigWaitHandler(ctx, apiClient, model.InstanceId, model.JobName, model.ProjectId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting scrape config", func() error { + _, err = wait.DeleteScrapeConfigWaitHandler(ctx, apiClient.DefaultAPI, model.InstanceId, model.JobName, model.ProjectId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for scrape config deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -122,7 +121,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiDeleteScrapeConfigRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiDeleteScrapeConfigRequest { req := apiClient.DeleteScrapeConfig(ctx, model.InstanceId, model.JobName, model.ProjectId) return req } diff --git a/internal/cmd/observability/scrape-config/delete/delete_test.go b/internal/cmd/observability/scrape-config/delete/delete_test.go index c9549964d..be623a878 100644 --- a/internal/cmd/observability/scrape-config/delete/delete_test.go +++ b/internal/cmd/observability/scrape-config/delete/delete_test.go @@ -9,7 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -17,7 +17,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testJobName = "my-config" @@ -59,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiDeleteScrapeConfigRequest)) observability.ApiDeleteScrapeConfigRequest { - request := testClient.DeleteScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) + request := testClient.DefaultAPI.DeleteScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) for _, mod := range mods { mod(&request) } @@ -219,10 +219,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/observability/scrape-config/describe/describe.go b/internal/cmd/observability/scrape-config/describe/describe.go index 0b7eb0eb5..edf2427b6 100644 --- a/internal/cmd/observability/scrape-config/describe/describe.go +++ b/internal/cmd/observability/scrape-config/describe/describe.go @@ -2,13 +2,14 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", jobNameArg), Short: "Shows details of a scrape configuration from an Observability instance", @@ -60,13 +60,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("read scrape configuration: %w", err) } - return outputResult(params.Printer, model.OutputFormat, resp.Data) + return outputResult(params.Printer, model.OutputFormat, &resp.Data) }, } configureFlags(cmd) @@ -95,34 +95,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiGetScrapeConfigRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiGetScrapeConfigRequest { req := apiClient.GetScrapeConfig(ctx, model.InstanceId, model.JobName, model.ProjectId) return req } func outputResult(p *print.Printer, outputFormat string, config *observability.Job) error { - if config == nil { - return fmt.Errorf(`config is nil`) - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(config, "", " ") - if err != nil { - return fmt.Errorf("marshal scrape configuration: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(config, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal scrape configuration: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, config, func() error { saml2Enabled := "Enabled" if config.Params != nil { saml2 := (*config.Params)["saml2"] @@ -133,7 +112,7 @@ func outputResult(p *print.Printer, outputFormat string, config *observability.J var targets []string if config.StaticConfigs != nil { - for _, target := range *config.StaticConfigs { + for _, target := range config.StaticConfigs { targetLabels := []string{} targetLabelStr := "N/A" if target.Labels != nil { @@ -147,22 +126,22 @@ func outputResult(p *print.Printer, outputFormat string, config *observability.J } targetUrlsStr := "N/A" if target.Targets != nil { - targetUrlsStr = strings.Join(*target.Targets, ",") + targetUrlsStr = strings.Join(target.Targets, ",") } targets = append(targets, fmt.Sprintf("labels: %s\nurls: %s", targetLabelStr, targetUrlsStr)) } } table := tables.NewTable() - table.AddRow("NAME", utils.PtrString(config.JobName)) + table.AddRow("NAME", config.JobName) table.AddSeparator() table.AddRow("METRICS PATH", utils.PtrString(config.MetricsPath)) table.AddSeparator() table.AddRow("SCHEME", utils.PtrString(config.Scheme)) table.AddSeparator() - table.AddRow("SCRAPE INTERVAL", utils.PtrString(config.ScrapeInterval)) + table.AddRow("SCRAPE INTERVAL", config.ScrapeInterval) table.AddSeparator() - table.AddRow("SCRAPE TIMEOUT", utils.PtrString(config.ScrapeTimeout)) + table.AddRow("SCRAPE TIMEOUT", config.ScrapeTimeout) table.AddSeparator() table.AddRow("SAML2", saml2Enabled) table.AddSeparator() @@ -171,9 +150,9 @@ func outputResult(p *print.Printer, outputFormat string, config *observability.J } else { table.AddRow("AUTHENTICATION", "Basic Auth") table.AddSeparator() - table.AddRow("USERNAME", utils.PtrString(config.BasicAuth.Username)) + table.AddRow("USERNAME", config.BasicAuth.Username) table.AddSeparator() - table.AddRow("PASSWORD", utils.PtrString(config.BasicAuth.Password)) + table.AddRow("PASSWORD", config.BasicAuth.Password) } table.AddSeparator() for i, target := range targets { @@ -187,5 +166,5 @@ func outputResult(p *print.Printer, outputFormat string, config *observability.J } return nil - } + }) } diff --git a/internal/cmd/observability/scrape-config/describe/describe_test.go b/internal/cmd/observability/scrape-config/describe/describe_test.go index d975ba82c..23cbc4c2f 100644 --- a/internal/cmd/observability/scrape-config/describe/describe_test.go +++ b/internal/cmd/observability/scrape-config/describe/describe_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testJobName = "my-config" @@ -61,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiGetScrapeConfigRequest)) observability.ApiGetScrapeConfigRequest { - request := testClient.GetScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) + request := testClient.DefaultAPI.GetScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) for _, mod := range mods { mod(&request) } @@ -221,10 +220,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -237,7 +236,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string - config *observability.Job + config observability.Job } tests := []struct { name string @@ -245,23 +244,21 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, - wantErr: true, + name: "empty", + args: args{}, }, { name: "empty config", args: args{ - config: &observability.Job{}, + config: observability.Job{}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.config); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, &tt.args.config); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/scrape-config/generate-payload/generate_payload.go b/internal/cmd/observability/scrape-config/generate-payload/generate_payload.go index 487fa32b3..ab489abe4 100644 --- a/internal/cmd/observability/scrape-config/generate-payload/generate_payload.go +++ b/internal/cmd/observability/scrape-config/generate-payload/generate_payload.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/fileutils" @@ -16,7 +17,7 @@ import ( observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -32,7 +33,7 @@ type inputModel struct { FilePath *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "generate-payload", Short: "Generates a payload to create/update scrape configurations for an Observability instance ", @@ -60,9 +61,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Generate an Update payload with the values of an existing configuration named "my-config" for Observability instance xxx, and preview it in the terminal`, `$ stackit observability scrape-config generate-payload --job-name my-config --instance-id xxx`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -78,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return outputCreateResult(params.Printer, model.FilePath, &createPayload) } - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("read Observability scrape config: %w", err) @@ -102,7 +103,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(filePathFlag, "f", "", "If set, writes the payload to the given file. If unset, writes the payload to the standard output") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) jobName := flags.FlagToStringPointer(p, cmd, jobNameFlag) @@ -120,7 +121,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiGetScrapeConfigRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiGetScrapeConfigRequest { req := apiClient.GetScrapeConfig(ctx, model.InstanceId, *model.JobName, model.ProjectId) return req } @@ -130,7 +131,7 @@ func outputCreateResult(p *print.Printer, filePath *string, payload *observabili return fmt.Errorf("payload is nil") } - payloadBytes, err := json.MarshalIndent(*payload, "", " ") + payloadBytes, err := json.MarshalIndent(*payload, "", " ") // nolint:gosec // false positive if err != nil { return fmt.Errorf("marshal payload: %w", err) } @@ -152,7 +153,7 @@ func outputUpdateResult(p *print.Printer, filePath *string, payload *observabili return fmt.Errorf("payload is nil") } - payloadBytes, err := json.MarshalIndent(*payload, "", " ") + payloadBytes, err := json.MarshalIndent(*payload, "", " ") // nolint:gosec // false positive if err != nil { return fmt.Errorf("marshal payload: %w", err) } diff --git a/internal/cmd/observability/scrape-config/generate-payload/generate_payload_test.go b/internal/cmd/observability/scrape-config/generate-payload/generate_payload_test.go index e9a3a076b..f59b3d66b 100644 --- a/internal/cmd/observability/scrape-config/generate-payload/generate_payload_test.go +++ b/internal/cmd/observability/scrape-config/generate-payload/generate_payload_test.go @@ -4,15 +4,16 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +21,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -59,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiGetScrapeConfigRequest)) observability.ApiGetScrapeConfigRequest { - request := testClient.GetScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) + request := testClient.DefaultAPI.GetScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) for _, mod := range mods { mod(&request) } @@ -69,6 +70,7 @@ func fixtureRequest(mods ...func(request *observability.ApiGetScrapeConfigReques func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -171,53 +173,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -238,10 +194,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -274,11 +230,10 @@ func TestOutputCreateResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputCreateResult(p, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { + if err := outputCreateResult(params.Printer, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { t.Errorf("outputCreateResult() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -308,11 +263,10 @@ func TestOutputUpdateResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputUpdateResult(p, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { + if err := outputUpdateResult(params.Printer, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { t.Errorf("outputUpdateResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/scrape-config/list/list.go b/internal/cmd/observability/scrape-config/list/list.go index 95a9770cc..2af2c8d82 100644 --- a/internal/cmd/observability/scrape-config/list/list.go +++ b/internal/cmd/observability/scrape-config/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,12 +14,11 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" observabilityUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -34,7 +32,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all scrape configurations of an Observability instance", @@ -51,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 scrape configurations of Observability instance "xxx"`, "$ stackit observability scrape-config list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -65,20 +63,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) resp, err := req.Execute() if err != nil { return fmt.Errorf("get scrape configurations: %w", err) } - configs := *resp.Data - if len(configs) == 0 { - instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient, model.InstanceId, model.ProjectId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId - } - params.Printer.Info("No scrape configurations found for instance %q\n", instanceLabel) - return nil + + configs := resp.GetData() + + instanceLabel, err := observabilityUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.InstanceId, model.ProjectId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId } // Truncate output @@ -86,7 +82,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { configs = configs[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, configs) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, configs) }, } @@ -102,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,30 +119,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiListScrapeConfigsRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiListScrapeConfigsRequest { req := apiClient.ListScrapeConfigs(ctx, model.InstanceId, model.ProjectId) return req } -func outputResult(p *print.Printer, outputFormat string, configs []observability.Job) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(configs, "", " ") - if err != nil { - return fmt.Errorf("marshal scrape configurations list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, configs []observability.Job) error { + return p.OutputResult(outputFormat, configs, func() error { + if len(configs) == 0 { + p.Outputf("No scrape configurations found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(configs, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal scrape configurations list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("NAME", "TARGETS", "SCRAPE INTERVAL") for i := range configs { @@ -154,18 +137,18 @@ func outputResult(p *print.Printer, outputFormat string, configs []observability targets := 0 if c.StaticConfigs != nil { - for _, sc := range *c.StaticConfigs { + for _, sc := range c.StaticConfigs { if sc.Targets == nil { continue } - targets += len(*sc.Targets) + targets += len(sc.Targets) } } table.AddRow( - utils.PtrString(c.JobName), + c.JobName, targets, - utils.PtrString(c.ScrapeInterval), + c.ScrapeInterval, ) } err := table.Display(p) @@ -174,5 +157,5 @@ func outputResult(p *print.Printer, outputFormat string, configs []observability } return nil - } + }) } diff --git a/internal/cmd/observability/scrape-config/list/list_test.go b/internal/cmd/observability/scrape-config/list/list_test.go index 35e53001b..f993e0a1c 100644 --- a/internal/cmd/observability/scrape-config/list/list_test.go +++ b/internal/cmd/observability/scrape-config/list/list_test.go @@ -4,16 +4,16 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +21,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -53,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiListScrapeConfigsRequest)) observability.ApiListScrapeConfigsRequest { - request := testClient.ListScrapeConfigs(testCtx, testInstanceId, testProjectId) + request := testClient.DefaultAPI.ListScrapeConfigs(testCtx, testInstanceId, testProjectId) for _, mod := range mods { mod(&request) } @@ -63,6 +63,7 @@ func fixtureRequest(mods ...func(request *observability.ApiListScrapeConfigsRequ func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -138,47 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -198,10 +159,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -213,8 +174,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - configs []observability.Job + outputFormat string + instanceLabel string + configs []observability.Job } tests := []struct { name string @@ -241,11 +203,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.configs); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.configs); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/observability/scrape-config/scrape_config.go b/internal/cmd/observability/scrape-config/scrape_config.go index d0934ff3b..b45cff386 100644 --- a/internal/cmd/observability/scrape-config/scrape_config.go +++ b/internal/cmd/observability/scrape-config/scrape_config.go @@ -7,14 +7,14 @@ import ( generatepayload "github.com/stackitcloud/stackit-cli/internal/cmd/observability/scrape-config/generate-payload" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/scrape-config/list" "github.com/stackitcloud/stackit-cli/internal/cmd/observability/scrape-config/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "scrape-config", Short: "Provides functionality for scrape configurations in Observability", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(generatepayload.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/observability/scrape-config/update/update.go b/internal/cmd/observability/scrape-config/update/update.go index ab39ea9e0..e5eb24e06 100644 --- a/internal/cmd/observability/scrape-config/update/update.go +++ b/internal/cmd/observability/scrape-config/update/update.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/observability/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) const ( @@ -32,7 +33,7 @@ type inputModel struct { Payload observability.UpdateScrapeConfigPayload } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", jobNameArg), Short: "Updates a scrape configuration of an Observability instance", @@ -68,16 +69,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update scrape configuration %q?", model.JobName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update scrape configuration %q?", model.JobName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req := buildRequest(ctx, model, apiClient) + req := buildRequest(ctx, model, apiClient.DefaultAPI) _, err = req.Execute() if err != nil { return fmt.Errorf("update scrape config: %w", err) @@ -123,7 +122,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu }, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *observability.APIClient) observability.ApiUpdateScrapeConfigRequest { +func buildRequest(ctx context.Context, model *inputModel, apiClient observability.DefaultAPI) observability.ApiUpdateScrapeConfigRequest { req := apiClient.UpdateScrapeConfig(ctx, model.InstanceId, model.JobName, model.ProjectId) req = req.UpdateScrapeConfigPayload(model.Payload) diff --git a/internal/cmd/observability/scrape-config/update/update_test.go b/internal/cmd/observability/scrape-config/update/update_test.go index 595b4f09e..5a41cfbe9 100644 --- a/internal/cmd/observability/scrape-config/update/update_test.go +++ b/internal/cmd/observability/scrape-config/update/update_test.go @@ -10,7 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -18,35 +18,39 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &observability.APIClient{} +var testClient = &observability.APIClient{DefaultAPI: &observability.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testJobName = "my-config" var testPayload = observability.UpdateScrapeConfigPayload{ - BasicAuth: &observability.CreateScrapeConfigPayloadBasicAuth{ + BasicAuth: &observability.UpdateScrapeConfigPayloadBasicAuth{ Username: utils.Ptr("username"), Password: utils.Ptr("password"), }, BearerToken: utils.Ptr("bearerToken"), HonorLabels: utils.Ptr(true), HonorTimeStamps: utils.Ptr(true), - MetricsPath: utils.Ptr("/metrics"), - MetricsRelabelConfigs: &[]observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner{ + MetricsPath: "/metrics", + MetricsRelabelConfigs: []observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner{ { - Action: observability.CREATESCRAPECONFIGPAYLOADMETRICSRELABELCONFIGSINNERACTION_REPLACE.Ptr(), - Modulus: utils.Ptr(1.0), + Action: observability.UPDATESCRAPECONFIGPAYLOADMETRICSRELABELCONFIGSINNERACTION_REPLACE.Ptr(), + Modulus: utils.Ptr(float32(1.0)), Regex: utils.Ptr("regex"), Replacement: utils.Ptr("replacement"), Separator: utils.Ptr("separator"), - SourceLabels: &[]string{"sourceLabel"}, + SourceLabels: []string{"sourceLabel"}, TargetLabel: utils.Ptr("targetLabel"), }, }, - Params: &map[string]interface{}{ - "key": []interface{}{string("value1"), string("value2")}, + Params: map[string]interface{}{ + "key": []interface{}{"value1", "value2"}, "key2": []interface{}{}, }, + ScrapeInterval: "5m", + ScrapeTimeout: "10s", + Scheme: "https", + StaticConfigs: []observability.UpdateScrapeConfigPayloadStaticConfigsInner{}, } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -86,7 +90,11 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st "params": { "key": ["value1", "value2"], "key2": [] - } + }, + "scheme": "https", + "scrapeInterval": "5m", + "scrapeTimeout": "10s", + "staticConfigs": [] }`, } for _, mod := range mods { @@ -112,7 +120,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *observability.ApiUpdateScrapeConfigRequest)) observability.ApiUpdateScrapeConfigRequest { - request := testClient.UpdateScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) + request := testClient.DefaultAPI.UpdateScrapeConfig(testCtx, testInstanceId, testJobName, testProjectId) request = request.UpdateScrapeConfigPayload(testPayload) for _, mod := range mods { mod(&request) @@ -262,7 +270,9 @@ func TestParseInput(t *testing.T) { if !tt.isValid { t.Fatalf("did not fail on invalid input") } - diff := cmp.Diff(model, tt.expectedModel) + diff := cmp.Diff(model, tt.expectedModel, cmp.FilterPath(func(path cmp.Path) bool { + return path.Last().String() == ".AdditionalProperties" + }, cmp.Ignore())) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -286,10 +296,10 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildRequest(testCtx, tt.model, testClient.DefaultAPI) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, observability.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/opensearch/credentials/create/create.go b/internal/cmd/opensearch/credentials/create/create.go index fdc2760f7..7e9eb5ee7 100644 --- a/internal/cmd/opensearch/credentials/create/create.go +++ b/internal/cmd/opensearch/credentials/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { ShowPassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for an OpenSearch instance", @@ -45,9 +44,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for an OpenSearch instance and show the password in the output`, "$ stackit opensearch credentials create --instance-id xxx --show-password"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -58,18 +57,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -79,7 +76,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create OpenSearch credentials: %w", err) } - return outputResult(params.Printer, model.OutputFormat, model.ShowPassword, instanceLabel, resp) + return outputResult(params.Printer, *model, instanceLabel, resp) }, } configureFlags(cmd) @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -106,64 +103,43 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ShowPassword: flags.FlagToBoolValue(p, cmd, showPasswordFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.Region, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, showPassword bool, instanceLabel string, resp *opensearch.CredentialsResponse) error { - if resp == nil || resp.Raw == nil || resp.Raw.Credentials == nil || resp.Uri == nil { +func outputResult(p *print.Printer, model inputModel, instanceLabel string, resp *opensearch.CredentialsResponse) error { + if model.GlobalFlagModel == nil { + return fmt.Errorf("no global flags defined") + } + if resp == nil || resp.Raw == nil { return fmt.Errorf("response or response content is nil") } - if !showPassword { - resp.Raw.Credentials.Password = utils.Ptr("hidden") + if !model.ShowPassword { + resp.Raw.Credentials.Password = "hidden" } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch credentials: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, utils.PtrString(resp.Id)) + return p.OutputResult(model.OutputFormat, resp, func() error { + p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, resp.Id) // The username field cannot be set by the user so we only display it if it's not returned empty - if resp.HasRaw() && resp.Raw.Credentials != nil { - if username := resp.Raw.Credentials.Username; username != nil && *username != "" { - p.Outputf("Username: %s\n", *username) + if resp.HasRaw() { + if username := resp.Raw.Credentials.Username; username != "" { + p.Outputf("Username: %s\n", username) } - if !showPassword { + if !model.ShowPassword { p.Outputf("Password: \n") } else { - p.Outputf("Password: %s\n", utils.PtrString(resp.Raw.Credentials.Password)) + p.Outputf("Password: %s\n", resp.Raw.Credentials.Password) } - p.Outputf("Host: %s\n", utils.PtrString(resp.Raw.Credentials.Host)) - p.Outputf("Port: %s\n", utils.PtrString(resp.Raw.Credentials.Port)) + p.Outputf("Host: %s\n", resp.Raw.Credentials.Host) + p.Outputf("Port: %d\n", resp.Raw.Credentials.Port) } - p.Outputf("URI: %s\n", *resp.Uri) + p.Outputf("URI: %s\n", resp.Uri) return nil - } + }) } diff --git a/internal/cmd/opensearch/credentials/create/create_test.go b/internal/cmd/opensearch/credentials/create/create_test.go index 6928feefc..f6cd78b76 100644 --- a/internal/cmd/opensearch/credentials/create/create_test.go +++ b/internal/cmd/opensearch/credentials/create/create_test.go @@ -7,26 +7,28 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = uuid.NewString() +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, } for _, mod := range mods { mod(flagValues) @@ -38,6 +40,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -49,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiCreateCredentialsRequest)) opensearch.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -59,6 +62,7 @@ func fixtureRequest(mods ...func(request *opensearch.ApiCreateCredentialsRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -87,21 +91,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -130,46 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -193,7 +158,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -204,8 +169,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - showPassword bool + model inputModel instanceLabel string resp *opensearch.CredentialsResponse } @@ -220,40 +184,20 @@ func TestOutputResult(t *testing.T) { wantErr: true, }, { - name: "set no raw in response", - args: args{ - resp: &opensearch.CredentialsResponse{ - Uri: utils.Ptr("https://opensearch.example.com"), - }, - }, - wantErr: true, - }, - { - name: "set empty raw in response", - args: args{ - resp: &opensearch.CredentialsResponse{ - Raw: &opensearch.RawCredentials{}, - Uri: utils.Ptr("https://opensearch.example.com"), - }, - }, - wantErr: true, - }, - { - name: "set raw but no uri in response", + name: "set uri but no raw in response", args: args{ resp: &opensearch.CredentialsResponse{ - Raw: &opensearch.RawCredentials{ - Credentials: &opensearch.Credentials{}, - }, + Uri: "https://opensearch.example.com", }, }, wantErr: true, }, { - name: "set uri but no raw in response", + name: "empty response should not cause panic when password should be hidden", args: args{ - resp: &opensearch.CredentialsResponse{ - Uri: utils.Ptr("https://opensearch.example.com"), + model: inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{}, + ShowPassword: false, }, }, wantErr: true, @@ -263,19 +207,21 @@ func TestOutputResult(t *testing.T) { args: args{ resp: &opensearch.CredentialsResponse{ Raw: &opensearch.RawCredentials{ - Credentials: &opensearch.Credentials{}, + Credentials: opensearch.Credentials{}, }, - Uri: utils.Ptr("https://opensearch.example.com"), + Uri: "https://opensearch.example.com", + }, + model: inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{}, }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.showPassword, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/opensearch/credentials/credentials.go b/internal/cmd/opensearch/credentials/credentials.go index 3e1c17ba8..e9c878d02 100644 --- a/internal/cmd/opensearch/credentials/credentials.go +++ b/internal/cmd/opensearch/credentials/credentials.go @@ -5,14 +5,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for OpenSearch credentials", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/opensearch/credentials/delete/delete.go b/internal/cmd/opensearch/credentials/delete/delete.go index faccd169e..15661c6cf 100644 --- a/internal/cmd/opensearch/credentials/delete/delete.go +++ b/internal/cmd/opensearch/credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsIdArg), Short: "Deletes credentials of an OpenSearch instance", @@ -55,24 +56,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - credentialsLabel, err := opensearchUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialsLabel, err := opensearchUtils.GetCredentialsUsername(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials user name: %v", err) credentialsLabel = model.CredentialsId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,19 +110,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) return req } diff --git a/internal/cmd/opensearch/credentials/delete/delete_test.go b/internal/cmd/opensearch/credentials/delete/delete_test.go index 350175fd2..1f236627a 100644 --- a/internal/cmd/opensearch/credentials/delete/delete_test.go +++ b/internal/cmd/opensearch/credentials/delete/delete_test.go @@ -4,25 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() -var testCredentialsId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = uuid.NewString() + testCredentialsId = uuid.NewString() +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -36,8 +36,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, } for _, mod := range mods { mod(flagValues) @@ -49,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -61,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiDeleteCredentialsRequest)) opensearch.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testRegion, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -105,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +123,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -165,54 +167,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +191,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/opensearch/credentials/describe/describe.go b/internal/cmd/opensearch/credentials/describe/describe.go index 92bdf6350..3d37ed7c1 100644 --- a/internal/cmd/opensearch/credentials/describe/describe.go +++ b/internal/cmd/opensearch/credentials/describe/describe.go @@ -2,11 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialsIdArg), Short: "Shows details of credentials of an OpenSearch instance", @@ -95,58 +95,39 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiGetCredentialsRequest { - req := apiClient.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) return req } func outputResult(p *print.Printer, outputFormat string, credentials *opensearch.CredentialsResponse) error { - if credentials == nil { - return fmt.Errorf("credentials is nil") - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch credentials: %w", err) + return p.OutputResult(outputFormat, credentials, func() error { + if credentials == nil { + return fmt.Errorf("credentials is nil") } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.AddRow("ID", utils.PtrString(credentials.Id)) + table.AddRow("ID", credentials.Id) table.AddSeparator() // The username field cannot be set by the user so we only display it if it's not returned empty - if credentials.HasRaw() && credentials.Raw.Credentials != nil { - if username := credentials.Raw.Credentials.Username; username != nil && *username != "" { - table.AddRow("USERNAME", *username) + if credentials.HasRaw() { + if username := credentials.Raw.Credentials.Username; username != "" { + table.AddRow("USERNAME", username) table.AddSeparator() } - table.AddRow("PASSWORD", utils.PtrString(credentials.Raw.Credentials.Password)) + table.AddRow("PASSWORD", credentials.Raw.Credentials.Password) table.AddSeparator() table.AddRow("URI", utils.PtrString(credentials.Raw.Credentials.Uri)) + table.AddSeparator() + table.AddRow("HOST", credentials.Raw.Credentials.Host) + hosts := credentials.Raw.Credentials.Hosts + if len(hosts) > 0 { + table.AddSeparator() + table.AddRow("HOSTS", strings.Join(hosts, "\n")) + } } err := table.Display(p) if err != nil { @@ -154,5 +135,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials *opensearch } return nil - } + }) } diff --git a/internal/cmd/opensearch/credentials/describe/describe_test.go b/internal/cmd/opensearch/credentials/describe/describe_test.go index 50ce776b5..7ac6737c3 100644 --- a/internal/cmd/opensearch/credentials/describe/describe_test.go +++ b/internal/cmd/opensearch/credentials/describe/describe_test.go @@ -4,25 +4,27 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() -var testCredentialsId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = uuid.NewString() + testCredentialsId = uuid.NewString() +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -36,8 +38,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, } for _, mod := range mods { mod(flagValues) @@ -49,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -61,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiGetCredentialsRequest)) opensearch.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testRegion, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -105,7 +109,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +117,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +125,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -165,54 +169,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +193,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -267,12 +224,39 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "host and hosts", + args: args{ + credentials: &opensearch.CredentialsResponse{ + Raw: &opensearch.RawCredentials{ + Credentials: opensearch.Credentials{ + Host: "host", + Hosts: []string{ + "hosts-a", + "hosts-b", + }, + }, + }, + }, + }, + }, + { + name: "raw credentials and nil hosts", + args: args{ + credentials: &opensearch.CredentialsResponse{ + Raw: &opensearch.RawCredentials{ + Credentials: opensearch.Credentials{ + Hosts: nil, + }, + }, + }, + }, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/opensearch/credentials/list/list.go b/internal/cmd/opensearch/credentials/list/list.go index 0efb58d37..85a083e71 100644 --- a/internal/cmd/opensearch/credentials/list/list.go +++ b/internal/cmd/opensearch/credentials/list/list.go @@ -2,10 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,11 +15,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" opensearchUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) const ( @@ -33,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials' IDs for an OpenSearch instance", @@ -50,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials' IDs for an OpenSearch instance`, "$ stackit opensearch credentials list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,22 +67,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list OpenSearch credentials: %w", err) } - credentials := *resp.CredentialsList - if len(credentials) == 0 { - instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId - } - params.Printer.Info("No credentials found for instance %q\n", instanceLabel) - return nil + credentials := resp.GetCredentialsList() + + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + + return outputResult(params.Printer, model.OutputFormat, instanceLabel, credentials) }, } configureFlags(cmd) @@ -99,7 +95,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -119,47 +115,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.Region, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []opensearch.CredentialsListItem) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, credentials []opensearch.CredentialsListItem) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID") for i := range credentials { c := credentials[i] - table.AddRow(utils.PtrString(c.Id)) + table.AddRow(c.Id) } err := table.Display(p) if err != nil { @@ -167,5 +143,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials []opensearc } return nil - } + }) } diff --git a/internal/cmd/opensearch/credentials/list/list_test.go b/internal/cmd/opensearch/credentials/list/list_test.go index bfe0ea554..d965e574a 100644 --- a/internal/cmd/opensearch/credentials/list/list_test.go +++ b/internal/cmd/opensearch/credentials/list/list_test.go @@ -4,31 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = uuid.NewString() +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -40,6 +42,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -52,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiListCredentialsRequest)) opensearch.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -62,6 +65,7 @@ func fixtureRequest(mods ...func(request *opensearch.ApiListCredentialsRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -80,21 +84,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,46 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -211,8 +176,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - credentials []opensearch.CredentialsListItem + outputFormat string + instanceLabel string + credentials []opensearch.CredentialsListItem } tests := []struct { name string @@ -239,11 +205,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index bc41bfbed..b7694de96 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -2,13 +2,12 @@ package create import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +21,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api/wait" ) const ( @@ -33,7 +32,6 @@ const ( metricsFrequencyFlag = "metrics-frequency" metricsPrefixFlag = "metrics-prefix" monitoringInstanceIdFlag = "monitoring-instance-id" - pluginFlag = "plugin" sgwAclFlag = "acl" syslogFlag = "syslog" planIdFlag = "plan-id" @@ -41,24 +39,26 @@ const ( versionFlag = "version" ) +var flagPlugins = flags.StringEnumSliceFlag("plugin", opensearch.AllowedInstanceParametersPluginsInnerEnumValues, "Plugins") + type inputModel struct { *globalflags.GlobalFlagModel PlanName string Version string - InstanceName *string + InstanceName string EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string - Plugin *[]string + Plugin []opensearch.InstanceParametersPluginsInner SgwAcl *[]string - Syslog *[]string - PlanId *string + Syslog []string + PlanId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates an OpenSearch instance", @@ -75,9 +75,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create an OpenSearch instance with name "my-instance" and specify IP range which is allowed to access it`, "$ stackit opensearch instance create --name my-instance --plan-id xxx --acl 1.2.3.0/24"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -94,16 +94,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create an OpenSearch instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create an OpenSearch instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -115,20 +113,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create OpenSearch instance: %w", err) } - instanceId := *resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, resp.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for OpenSearch instance creation: %w", err) } - s.Stop() } - return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, instanceId, resp) + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp.InstanceId, resp) }, } configureFlags(cmd) @@ -139,10 +136,10 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") - cmd.Flags().StringSlice(pluginFlag, []string{}, "Plugin") + flagPlugins.Register(cmd) cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().StringSlice(syslogFlag, []string{}, "Syslog") cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") @@ -153,22 +150,22 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } - planId := flags.FlagToStringPointer(p, cmd, planIdFlag) + planId := flags.FlagToStringValue(p, cmd, planIdFlag) planName := flags.FlagToStringValue(p, cmd, planNameFlag) version := flags.FlagToStringValue(p, cmd, versionFlag) - if planId == nil && (planName == "" || version == "") { + if planId == "" && (planName == "" || version == "") { return nil, &cliErr.DSAInputPlanError{ Cmd: cmd, } } - if planId != nil && (planName != "" || version != "") { + if planId != "" && (planName != "" || version != "") { return nil, &cliErr.DSAInputPlanError{ Cmd: cmd, } @@ -176,49 +173,41 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), EnableMonitoring: flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag), MonitoringInstanceId: flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag), Graphite: flags.FlagToStringPointer(p, cmd, graphiteFlag), - MetricsFrequency: flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag), + MetricsFrequency: flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag), MetricsPrefix: flags.FlagToStringPointer(p, cmd, metricsPrefixFlag), - Plugin: flags.FlagToStringSlicePointer(p, cmd, pluginFlag), + Plugin: flagPlugins.Get(), SgwAcl: flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag), - Syslog: flags.FlagToStringSlicePointer(p, cmd, syslogFlag), + Syslog: flags.FlagToStringSliceValue(p, cmd, syslogFlag), PlanId: planId, PlanName: planName, Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } type openSearchClient interface { - CreateInstance(ctx context.Context, projectId string) opensearch.ApiCreateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) + CreateInstance(ctx context.Context, projectId, region string) opensearch.ApiCreateInstanceRequest + ListOfferings(ctx context.Context, projectId, region string) opensearch.ApiListOfferingsRequest } func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchClient) (opensearch.ApiCreateInstanceRequest, error) { - req := apiClient.CreateInstance(ctx, model.ProjectId) + req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) - var planId *string + var planId string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get OpenSearch offerings: %w", err) } - if model.PlanId == nil { + if model.PlanId == "" { planId, err = opensearchUtils.LoadPlanId(model.PlanName, model.Version, offerings) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError @@ -228,7 +217,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchCl return req, err } } else { - err := opensearchUtils.ValidatePlanId(*model.PlanId, offerings) + err := opensearchUtils.ValidatePlanId(model.PlanId, offerings) if err != nil { return req, err } @@ -258,29 +247,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchCl } func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel, instanceId string, resp *opensearch.CreateInstanceResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, instanceId) return nil - } + }) } diff --git a/internal/cmd/opensearch/instance/create/create_test.go b/internal/cmd/opensearch/instance/create/create_test.go index e6028b4d9..ba50a359e 100644 --- a/internal/cmd/opensearch/instance/create/create_test.go +++ b/internal/cmd/opensearch/instance/create/create_test.go @@ -5,57 +5,58 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} - -type openSearchClientMocked struct { - returnError bool - listOfferingsResp *opensearch.ListOfferingsResponse -} +var testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} -func (c *openSearchClientMocked) CreateInstance(ctx context.Context, projectId string) opensearch.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId) +type mockSettings struct { + getOfferingsFails bool + getOfferingsResp *opensearch.ListOfferingsResponse } -func (c *openSearchClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*opensearch.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIClientMock(c mockSettings) opensearch.DefaultAPI { + return opensearch.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ opensearch.ApiListOfferingsRequest) (*opensearch.ListOfferingsResponse, error) { + if c.getOfferingsFails { + return nil, fmt.Errorf("list flavors failed") + } + return c.getOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var testProjectId = uuid.NewString() +var testRegion = "eu01" var testPlanId = uuid.NewString() var testMonitoringInstanceId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceNameFlag: "example-name", - enableMonitoringFlag: "true", - graphiteFlag: "example-graphite", - metricsFrequencyFlag: "100", - metricsPrefixFlag: "example-prefix", - monitoringInstanceIdFlag: testMonitoringInstanceId, - pluginFlag: "example-plugin", - sgwAclFlag: "198.51.100.14/24", - syslogFlag: "example-syslog", - planIdFlag: testPlanId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceNameFlag: "example-name", + enableMonitoringFlag: "true", + graphiteFlag: "example-graphite", + metricsFrequencyFlag: "100", + metricsPrefixFlag: "example-prefix", + monitoringInstanceIdFlag: testMonitoringInstanceId, + flagPlugins.Name(): string(opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE), + sgwAclFlag: "198.51.100.14/24", + syslogFlag: "example-syslog", + planIdFlag: testPlanId, } for _, mod := range mods { mod(flagValues) @@ -67,18 +68,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugin: utils.Ptr([]string{"example-plugin"}), + Plugin: []opensearch.InstanceParametersPluginsInner{opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE}, SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), - PlanId: utils.Ptr(testPlanId), + Syslog: []string{"example-syslog"}, + PlanId: testPlanId, } for _, mod := range mods { mod(model) @@ -87,20 +89,20 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiCreateInstanceRequest)) opensearch.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion) request = request.CreateInstancePayload(opensearch.CreateInstancePayload{ - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", Parameters: &opensearch.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugins: utils.Ptr([]string{"example-plugin"}), + Plugins: []opensearch.InstanceParametersPluginsInner{opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE}, SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }) for _, mod := range mods { mod(&request) @@ -111,6 +113,7 @@ func fixtureRequest(mods ...func(request *opensearch.ApiCreateInstanceRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string sgwAclValues []string pluginValues []string @@ -133,7 +136,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.PlanId = nil + model.PlanId = "" model.PlanName = "plan-name" model.Version = "6" }), @@ -146,9 +149,9 @@ func TestParseInput(t *testing.T) { { description: "required fields only", flagValues: map[string]string{ - projectIdFlag: testProjectId, - instanceNameFlag: "example-name", - planIdFlag: testPlanId, + globalflags.ProjectIdFlag: testProjectId, + instanceNameFlag: "example-name", + planIdFlag: testPlanId, }, isValid: true, expectedModel: &inputModel{ @@ -156,20 +159,20 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), - PlanId: utils.Ptr(testPlanId), + InstanceName: "example-name", + PlanId: testPlanId, }, }, { description: "zero values", flagValues: map[string]string{ - projectIdFlag: testProjectId, - planIdFlag: testPlanId, - instanceNameFlag: "", - enableMonitoringFlag: "false", - graphiteFlag: "", - metricsFrequencyFlag: "0", - metricsPrefixFlag: "", + globalflags.ProjectIdFlag: testProjectId, + planIdFlag: testPlanId, + instanceNameFlag: "", + enableMonitoringFlag: "false", + graphiteFlag: "", + metricsFrequencyFlag: "0", + metricsPrefixFlag: "", }, isValid: true, expectedModel: &inputModel{ @@ -177,32 +180,32 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - PlanId: utils.Ptr(testPlanId), - InstanceName: utils.Ptr(""), + PlanId: testPlanId, + InstanceName: "", EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -254,12 +257,11 @@ func TestParseInput(t *testing.T) { { description: "repeated plugin flags", flagValues: fixtureFlagValues(), - pluginValues: []string{"example-plugin-1", "example-plugin-2"}, + pluginValues: []string{string(opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE), string(opensearch.INSTANCEPARAMETERSPLUGINSINNER_ANALYSIS_PHONETIC)}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Plugin = utils.Ptr( - append(*model.Plugin, "example-plugin-1", "example-plugin-2"), - ) + model.Plugin = + append(model.Plugin, opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE, opensearch.INSTANCEPARAMETERSPLUGINSINNER_ANALYSIS_PHONETIC) }), }, { @@ -268,110 +270,45 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = + append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.pluginValues { - err := cmd.Flags().Set(pluginFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", pluginFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + flagPlugins.Name(): tt.pluginValues, + syslogFlag: tt.syslogValues, + }, tt.isValid) }) } } func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest opensearch.ApiCreateInstanceRequest - getOfferingsFails bool - getOfferingsResp *opensearch.ListOfferingsResponse - isValid bool + description string + model *inputModel + expectedRequest opensearch.ApiCreateInstanceRequest + mockClientSettings mockSettings + isValid bool }{ { description: "base", model: fixtureInputModel(), expectedRequest: fixtureRequest(), - getOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "example-plan-name", + Id: testPlanId, + }, }, }, }, @@ -382,20 +319,22 @@ func TestBuildRequest(t *testing.T) { description: "use plan name and version", model: fixtureInputModel( func(model *inputModel) { - model.PlanId = nil + model.PlanId = "" model.PlanName = "example-plan-name" model.Version = "example-version" }, ), expectedRequest: fixtureRequest(), - getOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "example-plan-name", + Id: testPlanId, + }, }, }, }, @@ -406,31 +345,35 @@ func TestBuildRequest(t *testing.T) { description: "get offering fails", model: fixtureInputModel( func(model *inputModel) { - model.PlanId = nil + model.PlanId = "" model.PlanName = "example-plan-name" model.Version = "example-version" }, ), - getOfferingsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + getOfferingsFails: true, + }, + isValid: false, }, { description: "plan name not found", model: fixtureInputModel( func(model *inputModel) { - model.PlanId = nil + model.PlanId = "" model.PlanName = "example-plan-name" model.Version = "example-version" }, ), - getOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "other-plan-name", + Id: testPlanId, + }, }, }, }, @@ -443,35 +386,34 @@ func TestBuildRequest(t *testing.T) { model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }, - getOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + getOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "example-plan-name", + Id: testPlanId, + }, }, }, }, }, }, - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(opensearch.CreateInstancePayload{PlanId: utils.Ptr(testPlanId), Parameters: &opensearch.InstanceParameters{}}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion). + CreateInstancePayload(opensearch.CreateInstancePayload{PlanId: testPlanId, Parameters: &opensearch.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &openSearchClientMocked{ - returnError: tt.getOfferingsFails, - listOfferingsResp: tt.getOfferingsResp, - } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIClientMock(tt.mockClientSettings)) if err != nil { if !tt.isValid { return @@ -482,6 +424,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -516,11 +459,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/opensearch/instance/delete/delete.go b/internal/cmd/opensearch/instance/delete/delete.go index e0ed0db20..34463c499 100644 --- a/internal/cmd/opensearch/instance/delete/delete.go +++ b/internal/cmd/opensearch/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes an OpenSearch instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for OpenSearch instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } diff --git a/internal/cmd/opensearch/instance/delete/delete_test.go b/internal/cmd/opensearch/instance/delete/delete_test.go index 217ff1b1d..29d85b31b 100644 --- a/internal/cmd/opensearch/instance/delete/delete_test.go +++ b/internal/cmd/opensearch/instance/delete/delete_test.go @@ -4,24 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = uuid.NewString() +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -35,7 +35,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -58,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiDeleteInstanceRequest)) opensearch.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -102,7 +104,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +112,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +120,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +140,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +164,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/opensearch/instance/describe/describe.go b/internal/cmd/opensearch/instance/describe/describe.go index da181a5a4..0bc1345b2 100644 --- a/internal/cmd/opensearch/instance/describe/describe.go +++ b/internal/cmd/opensearch/instance/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of an OpenSearch instance", @@ -83,61 +82,33 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } func outputResult(p *print.Printer, outputFormat string, instance *opensearch.Instance) error { - if instance == nil { - return fmt.Errorf("instance is nil") - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch instance: %w", err) + return p.OutputResult(outputFormat, instance, func() error { + if instance == nil { + return fmt.Errorf("instance is nil") } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.AddRow("ID", utils.PtrString(instance.InstanceId)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - if instance.LastOperation != nil { - table.AddRow("LAST OPERATION TYPE", utils.PtrString(instance.LastOperation.Type)) - table.AddSeparator() - table.AddRow("LAST OPERATION STATE", utils.PtrString(instance.LastOperation.State)) - table.AddSeparator() - } - table.AddRow("PLAN ID", utils.PtrString(instance.PlanId)) + table.AddRow("LAST OPERATION TYPE", instance.LastOperation.Type) + table.AddSeparator() + table.AddRow("LAST OPERATION STATE", instance.LastOperation.State) + table.AddSeparator() + table.AddRow("PLAN ID", instance.PlanId) // Only show ACL if it's present and not empty if instance.Parameters != nil { - acl := (*instance.Parameters)[aclParameterKey] + acl := instance.Parameters[aclParameterKey] aclStr, ok := acl.(string) if ok { if aclStr != "" { @@ -152,5 +123,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *opensearch.In } return nil - } + }) } diff --git a/internal/cmd/opensearch/instance/describe/describe_test.go b/internal/cmd/opensearch/instance/describe/describe_test.go index 6e17a8e85..dd5c0619a 100644 --- a/internal/cmd/opensearch/instance/describe/describe_test.go +++ b/internal/cmd/opensearch/instance/describe/describe_test.go @@ -4,24 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() -var testInstanceId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" + testInstanceId = uuid.NewString() +) func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiGetInstanceRequest)) opensearch.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -102,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -241,11 +197,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/opensearch/instance/instance.go b/internal/cmd/opensearch/instance/instance.go index 05d92bbc6..d8f58a668 100644 --- a/internal/cmd/opensearch/instance/instance.go +++ b/internal/cmd/opensearch/instance/instance.go @@ -6,14 +6,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance/update" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for OpenSearch instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/opensearch/instance/list/list.go b/internal/cmd/opensearch/instance/list/list.go index e32b425e5..48ce861df 100644 --- a/internal/cmd/opensearch/instance/list/list.go +++ b/internal/cmd/opensearch/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all OpenSearch instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 OpenSearch instances`, "$ stackit opensearch instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +66,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get OpenSearch instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + instances := resp.GetInstances() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,51 +110,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, instances []opensearch.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []opensearch.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "LAST OPERATION TYPE", "LAST OPERATION STATE") for i := range instances { instance := instances[i] table.AddRow( utils.PtrString(instance.InstanceId), - utils.PtrString(instance.Name), - utils.PtrString(instance.LastOperation.Type), - utils.PtrString(instance.LastOperation.State), + instance.Name, + instance.LastOperation.Type, + instance.LastOperation.State, ) } err := table.Display(p) @@ -166,5 +143,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []opensearch. } return nil - } + }) } diff --git a/internal/cmd/opensearch/instance/list/list_test.go b/internal/cmd/opensearch/instance/list/list_test.go index 766115689..c7d53f585 100644 --- a/internal/cmd/opensearch/instance/list/list_test.go +++ b/internal/cmd/opensearch/instance/list/list_test.go @@ -4,30 +4,31 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -39,6 +40,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Limit: utils.Ptr(int64(10)), @@ -50,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiListInstancesRequest)) opensearch.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +62,7 @@ func fixtureRequest(mods ...func(request *opensearch.ApiListInstancesRequest)) o func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +81,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -114,48 +117,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +141,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +153,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []opensearch.Instance } tests := []struct { @@ -218,11 +181,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/opensearch/instance/update/update.go b/internal/cmd/opensearch/instance/update/update.go index 254a9a41b..e5772dc63 100644 --- a/internal/cmd/opensearch/instance/update/update.go +++ b/internal/cmd/opensearch/instance/update/update.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api/wait" ) const ( @@ -32,7 +33,6 @@ const ( metricsFrequencyFlag = "metrics-frequency" metricsPrefixFlag = "metrics-prefix" monitoringInstanceIdFlag = "monitoring-instance-id" - pluginFlag = "plugin" sgwAclFlag = "acl" syslogFlag = "syslog" planIdFlag = "plan-id" @@ -40,6 +40,12 @@ const ( versionFlag = "version" ) +var flagPlugins = flags.StringEnumSliceFlag( + "plugin", + opensearch.AllowedInstanceParametersPluginsInnerEnumValues, + "Plugins", +) + type inputModel struct { *globalflags.GlobalFlagModel InstanceId string @@ -48,16 +54,16 @@ type inputModel struct { EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string - Plugin *[]string + Plugin []opensearch.InstanceParametersPluginsInner SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates an OpenSearch instance", @@ -84,22 +90,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := opensearchUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -115,13 +119,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for OpenSearch instance update: %w", err) } - s.Stop() } operationState := "Updated" @@ -139,10 +143,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") - cmd.Flags().StringSlice(pluginFlag, []string{}, "Plugin") + flagPlugins.Register(cmd) cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().StringSlice(syslogFlag, []string{}, "Syslog") cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") @@ -161,11 +165,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu enableMonitoring := flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag) monitoringInstanceId := flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag) graphite := flags.FlagToStringPointer(p, cmd, graphiteFlag) - metricsFrequency := flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag) + metricsFrequency := flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag) metricsPrefix := flags.FlagToStringPointer(p, cmd, metricsPrefixFlag) - plugin := flags.FlagToStringSlicePointer(p, cmd, pluginFlag) + plugin := flagPlugins.Get() sgwAcl := flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag) - syslog := flags.FlagToStringSlicePointer(p, cmd, syslogFlag) + syslog := flags.FlagToStringSliceValue(p, cmd, syslogFlag) planId := flags.FlagToStringPointer(p, cmd, planIdFlag) planName := flags.FlagToStringValue(p, cmd, planNameFlag) version := flags.FlagToStringValue(p, cmd, versionFlag) @@ -179,7 +183,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu if enableMonitoring == nil && monitoringInstanceId == nil && graphite == nil && metricsFrequency == nil && metricsPrefix == nil && plugin == nil && - sgwAcl == nil && syslog == nil && planId == nil && + sgwAcl == nil && planId == nil && planName == "" && version == "" { return nil, &cliErr.EmptyUpdateError{} } @@ -200,36 +204,28 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } type openSearchClient interface { - PartialUpdateInstance(ctx context.Context, projectId, instanceId string) opensearch.ApiPartialUpdateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*opensearch.ListOfferingsResponse, error) + PartialUpdateInstance(ctx context.Context, projectId, region, instanceId string) opensearch.ApiPartialUpdateInstanceRequest + ListOfferings(ctx context.Context, projectId, region string) opensearch.ApiListOfferingsRequest } func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchClient) (opensearch.ApiPartialUpdateInstanceRequest, error) { - req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.Region, model.InstanceId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get OpenSearch offerings: %w", err) } if model.PlanId == nil && model.PlanName != "" && model.Version != "" { - planId, err = opensearchUtils.LoadPlanId(model.PlanName, model.Version, offerings) + foundPlanId, err := opensearchUtils.LoadPlanId(model.PlanName, model.Version, offerings) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -237,13 +233,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient openSearchCl } return req, err } - } else { + planId = &foundPlanId + } else if model.PlanId != nil { // planId is not required for update operation - if model.PlanId != nil { - err := opensearchUtils.ValidatePlanId(*model.PlanId, offerings) - if err != nil { - return req, err - } + err := opensearchUtils.ValidatePlanId(*model.PlanId, offerings) + if err != nil { + return req, err } planId = model.PlanId } diff --git a/internal/cmd/opensearch/instance/update/update_test.go b/internal/cmd/opensearch/instance/update/update_test.go index 70168bbc2..4124a1af0 100644 --- a/internal/cmd/opensearch/instance/update/update_test.go +++ b/internal/cmd/opensearch/instance/update/update_test.go @@ -5,42 +5,40 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} +var testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} -type openSearchClientMocked struct { - returnError bool +type mockSettings struct { + getOfferingsFails bool listOfferingsResp *opensearch.ListOfferingsResponse } -func (c *openSearchClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId string) opensearch.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId) -} - -func (c *openSearchClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*opensearch.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIClientMock(c mockSettings) opensearch.DefaultAPI { + return opensearch.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ opensearch.ApiListOfferingsRequest) (*opensearch.ListOfferingsResponse, error) { + if c.getOfferingsFails { + return nil, fmt.Errorf("list flavors failed") + } + return c.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var ( testProjectId = uuid.NewString() + testRegion = "eu01" testInstanceId = uuid.NewString() testPlanId = uuid.NewString() testMonitoringInstanceId = uuid.NewString() @@ -58,16 +56,17 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - enableMonitoringFlag: "true", - graphiteFlag: "example-graphite", - metricsFrequencyFlag: "100", - metricsPrefixFlag: "example-prefix", - monitoringInstanceIdFlag: testMonitoringInstanceId, - pluginFlag: "example-plugin", - sgwAclFlag: "198.51.100.14/24", - syslogFlag: "example-syslog", - planIdFlag: testPlanId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + enableMonitoringFlag: "true", + graphiteFlag: "example-graphite", + metricsFrequencyFlag: "100", + metricsPrefixFlag: "example-prefix", + monitoringInstanceIdFlag: testMonitoringInstanceId, + flagPlugins.Name(): string(opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE), + sgwAclFlag: "198.51.100.14/24", + syslogFlag: "example-syslog", + planIdFlag: testPlanId, } for _, mod := range mods { mod(flagValues) @@ -79,17 +78,18 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugin: utils.Ptr([]string{"example-plugin"}), + Plugin: []opensearch.InstanceParametersPluginsInner{opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE}, SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -99,17 +99,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiPartialUpdateInstanceRequest)) opensearch.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId) request = request.PartialUpdateInstancePayload(opensearch.PartialUpdateInstancePayload{ Parameters: &opensearch.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugins: utils.Ptr([]string{"example-plugin"}), + Plugins: []opensearch.InstanceParametersPluginsInner{opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE}, SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, PlanId: utils.Ptr(testPlanId), }) @@ -159,7 +159,7 @@ func TestParseInput(t *testing.T) { description: "required flags only (no values to update)", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, }, isValid: false, expectedModel: &inputModel{ @@ -174,12 +174,12 @@ func TestParseInput(t *testing.T) { description: "zero values", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, - planIdFlag: testPlanId, - enableMonitoringFlag: "false", - graphiteFlag: "", - metricsFrequencyFlag: "0", - metricsPrefixFlag: "", + globalflags.ProjectIdFlag: testProjectId, + planIdFlag: testPlanId, + enableMonitoringFlag: "false", + graphiteFlag: "", + metricsFrequencyFlag: "0", + metricsPrefixFlag: "", }, isValid: true, expectedModel: &inputModel{ @@ -191,7 +191,7 @@ func TestParseInput(t *testing.T) { PlanId: utils.Ptr(testPlanId), EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -199,7 +199,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -207,7 +207,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -215,7 +215,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -270,12 +270,11 @@ func TestParseInput(t *testing.T) { description: "repeated plugin flags", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(), - pluginValues: []string{"example-plugin-1", "example-plugin-2"}, + pluginValues: []string{string(opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE), string(opensearch.INSTANCEPARAMETERSPLUGINSINNER_ANALYSIS_PHONETIC)}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Plugin = utils.Ptr( - append(*model.Plugin, "example-plugin-1", "example-plugin-2"), - ) + model.Plugin = + append(model.Plugin, opensearch.INSTANCEPARAMETERSPLUGINSINNER_REPOSITORY_AZURE, opensearch.INSTANCEPARAMETERSPLUGINSINNER_ANALYSIS_PHONETIC) }), }, { @@ -285,118 +284,45 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = + append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.pluginValues { - err := cmd.Flags().Set(pluginFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", pluginFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + flagPlugins.Name(): tt.pluginValues, + syslogFlag: tt.syslogValues, + }, tt.isValid) }) } } func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest opensearch.ApiPartialUpdateInstanceRequest - getOfferingsFails bool - listOfferingsResp *opensearch.ListOfferingsResponse - isValid bool + description string + model *inputModel + expectedRequest opensearch.ApiPartialUpdateInstanceRequest + mockClientSettings mockSettings + isValid bool }{ { description: "base", model: fixtureInputModel(), expectedRequest: fixtureRequest(), - listOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + listOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "example-plan-name", + Id: testPlanId, + }, }, }, }, @@ -413,14 +339,16 @@ func TestBuildRequest(t *testing.T) { }, ), expectedRequest: fixtureRequest(), - listOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + listOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "example-plan-name", + Id: testPlanId, + }, }, }, }, @@ -436,8 +364,10 @@ func TestBuildRequest(t *testing.T) { model.Version = "example-version" }, ), - getOfferingsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + getOfferingsFails: true, + }, + isValid: false, }, { description: "plan name not found", @@ -448,14 +378,16 @@ func TestBuildRequest(t *testing.T) { model.Version = "example-version" }, ), - listOfferingsResp: &opensearch.ListOfferingsResponse{ - Offerings: &[]opensearch.Offering{ - { - Version: utils.Ptr("example-version"), - Plans: &[]opensearch.Plan{ - { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + mockClientSettings: mockSettings{ + listOfferingsResp: &opensearch.ListOfferingsResponse{ + Offerings: []opensearch.Offering{ + { + Version: "example-version", + Plans: []opensearch.Plan{ + { + Name: "other-plan-name", + Id: testPlanId, + }, }, }, }, @@ -468,22 +400,19 @@ func TestBuildRequest(t *testing.T) { model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(opensearch.PartialUpdateInstancePayload{Parameters: &opensearch.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &openSearchClientMocked{ - returnError: tt.getOfferingsFails, - listOfferingsResp: tt.listOfferingsResp, - } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIClientMock(tt.mockClientSettings)) if err != nil { if !tt.isValid { return @@ -494,6 +423,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/opensearch/opensearch.go b/internal/cmd/opensearch/opensearch.go index 766065ed2..96d02fd3e 100644 --- a/internal/cmd/opensearch/opensearch.go +++ b/internal/cmd/opensearch/opensearch.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/credentials" "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/opensearch/plans" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "opensearch", Short: "Provides functionality for OpenSearch", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(plans.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) diff --git a/internal/cmd/opensearch/plans/plans.go b/internal/cmd/opensearch/plans/plans.go index c096fb230..b8fbce408 100644 --- a/internal/cmd/opensearch/plans/plans.go +++ b/internal/cmd/opensearch/plans/plans.go @@ -2,12 +2,13 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/opensearch/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists all OpenSearch service plans", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 OpenSearch service plans`, "$ stackit opensearch plans --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +65,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get OpenSearch service plans: %w", err) } - plans := *resp.Offerings - if len(plans) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No plans found for project %q\n", projectLabel) - return nil + plans := resp.GetOfferings() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +78,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { plans = plans[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, plans) + return outputResult(params.Printer, model.OutputFormat, projectLabel, plans) }, } @@ -94,7 +90,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,55 +109,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch.APIClient) opensearch.ApiListOfferingsRequest { - req := apiClient.ListOfferings(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListOfferings(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, plans []opensearch.Offering) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("marshal OpenSearch plans: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal OpenSearch plans: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []opensearch.Offering) error { + return p.OutputResult(outputFormat, plans, func() error { + if len(plans) == 0 { + p.Outputf("No plans found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] if o.Plans != nil { - for j := range *o.Plans { - plan := (*o.Plans)[j] + for j := range o.Plans { + plan := o.Plans[j] table.AddRow( - utils.PtrString(o.Name), - utils.PtrString(o.Version), - utils.PtrString(plan.Id), - utils.PtrString(plan.Name), - utils.PtrString(plan.Description), + o.Name, + o.Version, + plan.Id, + plan.Name, + plan.Description, ) } } @@ -174,5 +150,5 @@ func outputResult(p *print.Printer, outputFormat string, plans []opensearch.Offe } return nil - } + }) } diff --git a/internal/cmd/opensearch/plans/plans_test.go b/internal/cmd/opensearch/plans/plans_test.go index 0b4f95cec..349c30754 100644 --- a/internal/cmd/opensearch/plans/plans_test.go +++ b/internal/cmd/opensearch/plans/plans_test.go @@ -4,30 +4,31 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} -var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &opensearch.APIClient{} -var testProjectId = uuid.NewString() +var ( + testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + testClient = &opensearch.APIClient{DefaultAPI: &opensearch.DefaultAPIService{}} + testProjectId = uuid.NewString() + testRegion = "eu01" +) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -39,6 +40,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Limit: utils.Ptr(int64(10)), @@ -50,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *opensearch.ApiListOfferingsRequest)) opensearch.ApiListOfferingsRequest { - request := testClient.ListOfferings(testCtx, testProjectId) + request := testClient.DefaultAPI.ListOfferings(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +62,7 @@ func fixtureRequest(mods ...func(request *opensearch.ApiListOfferingsRequest)) o func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +81,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -114,48 +117,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +141,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, opensearch.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +153,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string plans []opensearch.Offering } tests := []struct { @@ -218,11 +181,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.plans); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/organization/describe/describe.go b/internal/cmd/organization/describe/describe.go new file mode 100644 index 000000000..6f11f1458 --- /dev/null +++ b/internal/cmd/organization/describe/describe.go @@ -0,0 +1,120 @@ +package describe + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + organizationIdArg = "ORGANIZATION_ID" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + OrganizationId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Show an organization", + Long: "Show an organization.", + // the arg can be the organization uuid or the container id, which is not a uuid, so no validation needed + Args: args.SingleArg(organizationIdArg, nil), + Example: examples.Build( + examples.NewExample( + `Describe the organization with the organization uuid "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`, + "$ stackit organization describe xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + ), + examples.NewExample( + `Describe the organization with the container id "foo-bar-organization"`, + "$ stackit organization describe foo-bar-organization", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return err + } + + return outputResult(params.Printer, model.OutputFormat, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + organizationId := inputArgs[0] + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + OrganizationId: organizationId, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiGetOrganizationRequest { + req := apiClient.DefaultAPI.GetOrganization(ctx, model.OrganizationId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, organization *resourcemanager.OrganizationResponse) error { + return p.OutputResult(outputFormat, organization, func() error { + if organization == nil { + p.Outputln("show organization: empty response") + return nil + } + + table := tables.NewTable() + + table.AddRow("ORGANIZATION ID", organization.OrganizationId) + table.AddSeparator() + table.AddRow("NAME", organization.Name) + table.AddSeparator() + table.AddRow("CONTAINER ID", organization.ContainerId) + table.AddSeparator() + table.AddRow("STATUS", organization.LifecycleState) + table.AddSeparator() + table.AddRow("CREATION TIME", organization.CreationTime) + table.AddSeparator() + table.AddRow("UPDATE TIME", organization.UpdateTime) + table.AddSeparator() + table.AddRow("LABELS", utils.JoinStringMap(utils.PtrValue(organization.Labels), ": ", ", ")) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/organization/describe/describe_test.go b/internal/cmd/organization/describe/describe_test.go new file mode 100644 index 000000000..f16371f6b --- /dev/null +++ b/internal/cmd/organization/describe/describe_test.go @@ -0,0 +1,188 @@ +package describe + +import ( + "context" + "testing" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} + +var ( + testOrganizationId = uuid.NewString() +) + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testOrganizationId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + OrganizationId: testOrganizationId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *resourcemanager.ApiGetOrganizationRequest)) resourcemanager.ApiGetOrganizationRequest { + request := testClient.DefaultAPI.GetOrganization(testCtx, testOrganizationId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "uuid as example for an organization id", + argValues: []string{"12345678-90ab-cdef-1234-1234567890ab"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.OrganizationId = "12345678-90ab-cdef-1234-1234567890ab" + }), + }, + { + description: "non uuid string as example for a container id", + argValues: []string{"foo-bar-organization"}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.OrganizationId = "foo-bar-organization" + }), + }, + { + description: "no args", + argValues: []string{}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest resourcemanager.ApiGetOrganizationRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + organization *resourcemanager.OrganizationResponse + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "nil pointer as organization", + args: args{ + organization: nil, + }, + wantErr: false, + }, + { + name: "empty organization", + args: args{ + organization: utils.Ptr(resourcemanager.OrganizationResponse{}), + }, + wantErr: false, + }, + { + name: "full response", + args: args{ + organization: utils.Ptr(resourcemanager.OrganizationResponse{ + OrganizationId: uuid.NewString(), + Name: "foo bar", + LifecycleState: resourcemanager.LIFECYCLESTATE_ACTIVE, + ContainerId: "foo-bar-organization", + CreationTime: time.Now(), + UpdateTime: time.Now(), + Labels: utils.Ptr(map[string]string{ + "foo": "true", + "bar": "false", + }), + }), + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.organization); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/organization/list/list.go b/internal/cmd/organization/list/list.go new file mode 100644 index 000000000..49b62c956 --- /dev/null +++ b/internal/cmd/organization/list/list.go @@ -0,0 +1,145 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 + Member string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all organizations", + Long: "Lists all organizations.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists organizations for your user`, + "$ stackit organization list", + ), + examples.NewExample( + `Lists the first 10 organizations`, + "$ stackit organization list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + model.Member = auth.GetProfileEmail(activeProfile) + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return err + } + + if resp == nil { + return fmt.Errorf("list organizations: empty response") + } + + return outputResult(params.Printer, model.OutputFormat, resp.Items) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list (default 50)") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && (*limit < 1 || *limit > 100) { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be between 0 and 100", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiListOrganizationsRequest { + req := apiClient.DefaultAPI.ListOrganizations(ctx) + req = req.Member(model.Member) + if model.Limit != nil { + req = req.Limit(float32(*model.Limit)) + } + return req +} + +func outputResult(p *print.Printer, outputFormat string, organizations []resourcemanager.ListOrganizationsResponseItemsInner) error { + return p.OutputResult(outputFormat, organizations, func() error { + if len(organizations) == 0 { + p.Outputln("No organizations found") + return nil + } + + table := tables.NewTable() + table.SetHeader("ID", "NAME", "CONTAINER ID") + + for _, organization := range organizations { + table.AddRow( + organization.OrganizationId, + organization.Name, + organization.ContainerId, + ) + table.AddSeparator() + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/organization/list/list_test.go b/internal/cmd/organization/list/list_test.go new file mode 100644 index 000000000..10e33beba --- /dev/null +++ b/internal/cmd/organization/list/list_test.go @@ -0,0 +1,188 @@ +package list + +import ( + "context" + "strconv" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} + +const ( + testEmail = "foo@bar" + testLimit = 10 +) + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + limitFlag: strconv.Itoa(testLimit), + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Limit: utils.Ptr(int64(testLimit)), + Member: testEmail, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *resourcemanager.ApiListOrganizationsRequest)) resourcemanager.ApiListOrganizationsRequest { + request := testClient.DefaultAPI.ListOrganizations(testCtx) + request = request.Limit(testLimit) + request = request.Member(testEmail) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + // model.Member is set by the Run function afterwards + model.Member = "" + }), + }, + { + description: "no limit", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, limitFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + // model.Member is set by the Run function afterwards + model.Member = "" + model.Limit = nil + }), + }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest resourcemanager.ApiListOrganizationsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + { + description: "empty input model", + model: fixtureInputModel(func(model *inputModel) { + model.Member = "" + model.Limit = nil + }), + expectedRequest: testClient.DefaultAPI.ListOrganizations(testCtx).Member(""), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + organizations []resourcemanager.ListOrganizationsResponseItemsInner + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: false, + }, + { + name: "empty organizations slice", + args: args{ + organizations: []resourcemanager.ListOrganizationsResponseItemsInner{}, + }, + wantErr: false, + }, + { + name: "empty organization in organizations slice", + args: args{ + organizations: []resourcemanager.ListOrganizationsResponseItemsInner{{}}, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.organizations); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/organization/member/add/add.go b/internal/cmd/organization/member/add/add.go index d3dd1f81b..325669ae3 100644 --- a/internal/cmd/organization/member/add/add.go +++ b/internal/cmd/organization/member/add/add.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/authorization/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) const ( @@ -33,7 +35,7 @@ type inputModel struct { Role *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("add %s", subjectArg), Short: "Adds a member to an organization", @@ -57,12 +59,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to add the %s role to %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to add the %s role to %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Configure API client @@ -106,15 +106,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Role: flags.FlagToStringPointer(p, cmd, roleFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/organization/member/add/add_test.go b/internal/cmd/organization/member/add/add_test.go index a98f06ecc..dfe3300f0 100644 --- a/internal/cmd/organization/member/add/add_test.go +++ b/internal/cmd/organization/member/add/add_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -125,54 +124,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/organization/member/list/list.go b/internal/cmd/organization/member/list/list.go index 6e414d257..3858c277e 100644 --- a/internal/cmd/organization/member/list/list.go +++ b/internal/cmd/organization/member/list/list.go @@ -2,13 +2,14 @@ package list import ( "context" - "encoding/json" "fmt" "sort" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,18 +19,23 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/authorization/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) const ( organizationIdFlag = "organization-id" subjectFlag = "subject" limitFlag = "limit" - sortByFlag = "sort-by" organizationResourceType = "organization" ) +var sortByFlag = flags.StringEnumFlag( + "sort-by", + []string{"subject", "role"}, + "Sort entries by a specific field,", + flags.StringEnumDefaultValue("subject"), +) + type inputModel struct { *globalflags.GlobalFlagModel @@ -39,7 +45,7 @@ type inputModel struct { SortBy string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists members of an organization", @@ -56,9 +62,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 members of an organization`, "$ stackit organization member list --organization-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -94,18 +100,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - sortByFlagOptions := []string{"subject", "role"} - cmd.Flags().String(organizationIdFlag, "", "The organization ID") cmd.Flags().String(subjectFlag, "", "Filter by subject (Identifier of user, service account or client. Usually email address in case of users or name in case of clients)") cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") - cmd.Flags().Var(flags.EnumFlag(false, "subject", sortByFlagOptions...), sortByFlag, fmt.Sprintf("Sort entries by a specific field, one of %q", sortByFlagOptions)) + sortByFlag.Register(cmd.Flags()) err := flags.MarkFlagsRequired(cmd, organizationIdFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) @@ -121,18 +125,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { OrganizationId: flags.FlagToStringPointer(p, cmd, organizationIdFlag), Subject: flags.FlagToStringPointer(p, cmd, subjectFlag), Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), - SortBy: flags.FlagWithDefaultToStringValue(p, cmd, sortByFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + SortBy: sortByFlag.Get(), } + p.DebugInputModel(model) return &model, nil } @@ -157,25 +153,7 @@ func outputResult(p *print.Printer, outputFormat, sortBy string, members []autho } sort.SliceStable(members, sortFn) - switch outputFormat { - case print.JSONOutputFormat: - // Show details - details, err := json.MarshalIndent(members, "", " ") - if err != nil { - return fmt.Errorf("marshal members: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(members, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal members: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, members, func() error { table := tables.NewTable() table.SetHeader("SUBJECT", "ROLE") for i := range members { @@ -187,9 +165,10 @@ func outputResult(p *print.Printer, outputFormat, sortBy string, members []autho table.AddRow(utils.PtrString(m.Subject), utils.PtrString(m.Role)) } - if sortBy == "subject" { + switch sortBy { + case "subject": table.EnableAutoMergeOnColumns(1) - } else if sortBy == "role" { + case "role": table.EnableAutoMergeOnColumns(2) } @@ -199,5 +178,5 @@ func outputResult(p *print.Printer, outputFormat, sortBy string, members []autho } return nil - } + }) } diff --git a/internal/cmd/organization/member/list/list_test.go b/internal/cmd/organization/member/list/list_test.go index ff90898b2..cd553c654 100644 --- a/internal/cmd/organization/member/list/list_test.go +++ b/internal/cmd/organization/member/list/list_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) @@ -56,6 +55,7 @@ func fixtureRequest(mods ...func(request *authorization.ApiListMembersRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -107,7 +107,7 @@ func TestParseInput(t *testing.T) { { description: "sort by role", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[sortByFlag] = "role" + flagValues[sortByFlag.Name()] = "role" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -117,7 +117,7 @@ func TestParseInput(t *testing.T) { { description: "sort by invalid", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[sortByFlag] = "invalid" + flagValues[sortByFlag.Name()] = "invalid" }), isValid: false, }, @@ -125,48 +125,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -237,11 +196,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.sortBy, tt.args.members); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.sortBy, tt.args.members); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/organization/member/member.go b/internal/cmd/organization/member/member.go index fe2b67e5d..bc4a8b200 100644 --- a/internal/cmd/organization/member/member.go +++ b/internal/cmd/organization/member/member.go @@ -4,14 +4,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/organization/member/add" "github.com/stackitcloud/stackit-cli/internal/cmd/organization/member/list" "github.com/stackitcloud/stackit-cli/internal/cmd/organization/member/remove" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "member", Short: "Manages organization members", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(add.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(remove.NewCmd(params)) diff --git a/internal/cmd/organization/member/remove/remove.go b/internal/cmd/organization/member/remove/remove.go index 6281b4214..27e95be67 100644 --- a/internal/cmd/organization/member/remove/remove.go +++ b/internal/cmd/organization/member/remove/remove.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -36,7 +37,7 @@ type inputModel struct { Force bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("remove %s", subjectArg), Short: "Removes a member from an organization", @@ -67,15 +68,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to remove the %s role from %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) - if model.Force { - prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) - } - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to remove the %s role from %s on organization with ID %q?", *model.Role, model.Subject, *model.OrganizationId) + if model.Force { + prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) + } + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -115,15 +114,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Force: flags.FlagToBoolValue(p, cmd, forceFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/organization/member/remove/remove_test.go b/internal/cmd/organization/member/remove/remove_test.go index 1b2a3e702..81f1a368c 100644 --- a/internal/cmd/organization/member/remove/remove_test.go +++ b/internal/cmd/organization/member/remove/remove_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -138,54 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/organization/organization.go b/internal/cmd/organization/organization.go index e7d1376b5..adc9fa8e8 100644 --- a/internal/cmd/organization/organization.go +++ b/internal/cmd/organization/organization.go @@ -3,16 +3,19 @@ package organization import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/stackitcloud/stackit-cli/internal/cmd/organization/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/organization/list" "github.com/stackitcloud/stackit-cli/internal/cmd/organization/member" "github.com/stackitcloud/stackit-cli/internal/cmd/organization/role" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "organization", Short: "Manages organizations", @@ -27,7 +30,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(member.NewCmd(params)) cmd.AddCommand(role.NewCmd(params)) + cmd.AddCommand(list.NewCmd(params)) + cmd.AddCommand(describe.NewCmd(params)) } diff --git a/internal/cmd/organization/role/list/list.go b/internal/cmd/organization/role/list/list.go index c16d145c2..f9e7e4bf7 100644 --- a/internal/cmd/organization/role/list/list.go +++ b/internal/cmd/organization/role/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/authorization/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) const ( @@ -34,7 +34,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists roles and permissions of an organization", @@ -51,9 +51,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 roles and permissions of an organization`, "$ stackit organization role list --organization-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -96,7 +96,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) @@ -113,15 +113,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -130,25 +122,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *authorizati } func outputRolesResult(p *print.Printer, outputFormat string, roles []authorization.Role) error { - switch outputFormat { - case print.JSONOutputFormat: - // Show details - details, err := json.MarshalIndent(roles, "", " ") - if err != nil { - return fmt.Errorf("marshal roles: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(roles, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal roles: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, roles, func() error { table := tables.NewTable() table.SetHeader("ROLE NAME", "ROLE DESCRIPTION", "PERMISSION NAME", "PERMISSION DESCRIPTION") for i := range roles { @@ -173,5 +147,5 @@ func outputRolesResult(p *print.Printer, outputFormat string, roles []authorizat } return nil - } + }) } diff --git a/internal/cmd/organization/role/list/list_test.go b/internal/cmd/organization/role/list/list_test.go index 5633b3f60..4a758d7a9 100644 --- a/internal/cmd/organization/role/list/list_test.go +++ b/internal/cmd/organization/role/list/list_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) @@ -55,6 +54,7 @@ func fixtureRequest(mods ...func(request *authorization.ApiListRolesRequest)) au func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -95,48 +95,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,11 +158,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputRolesResult(p, tt.args.outputFormat, tt.args.roles); (err != nil) != tt.wantErr { + if err := outputRolesResult(params.Printer, tt.args.outputFormat, tt.args.roles); (err != nil) != tt.wantErr { t.Errorf("outputRolesResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/organization/role/role.go b/internal/cmd/organization/role/role.go index 286783661..d3146aca8 100644 --- a/internal/cmd/organization/role/role.go +++ b/internal/cmd/organization/role/role.go @@ -2,14 +2,14 @@ package role import ( "github.com/stackitcloud/stackit-cli/internal/cmd/organization/role/list" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "role", Short: "Manages organization roles", @@ -21,6 +21,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) } diff --git a/internal/cmd/postgresflex/backup/backup.go b/internal/cmd/postgresflex/backup/backup.go index bac6c4a72..f6ad7c518 100644 --- a/internal/cmd/postgresflex/backup/backup.go +++ b/internal/cmd/postgresflex/backup/backup.go @@ -1,17 +1,17 @@ package backup import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/backup/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/backup/list" updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/backup/update-schedule" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "backup", Short: "Provides functionality for PostgreSQL Flex instance backups", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(updateschedule.NewCmd(params)) diff --git a/internal/cmd/postgresflex/backup/describe/describe.go b/internal/cmd/postgresflex/backup/describe/describe.go index 5e0ecf5c9..effe99ea0 100644 --- a/internal/cmd/postgresflex/backup/describe/describe.go +++ b/internal/cmd/postgresflex/backup/describe/describe.go @@ -2,13 +2,14 @@ package describe import ( "context" - "encoding/json" "fmt" "time" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( @@ -38,7 +38,7 @@ type inputModel struct { BackupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", backupIdArg), Short: "Shows details of a backup for a PostgreSQL Flex instance", @@ -73,7 +73,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("describe backup for PostgreSQL Flex instance: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp.Item) + return outputResult(params.Printer, model.OutputFormat, resp.Item) }, } configureFlags(cmd) @@ -103,11 +103,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiGetBackupRequest { - req := apiClient.GetBackup(ctx, model.ProjectId, model.Region, model.InstanceId, model.BackupId) + req := apiClient.DefaultAPI.GetBackup(ctx, model.ProjectId, model.Region, model.InstanceId, model.BackupId) return req } -func outputResult(p *print.Printer, outputFormat string, backup postgresflex.Backup) error { +func outputResult(p *print.Printer, outputFormat string, backup *postgresflex.Backup) error { + if backup == nil { + return fmt.Errorf("backup is nil") + } if backup.StartTime == nil || *backup.StartTime == "" { return fmt.Errorf("start time not defined") } @@ -117,24 +120,7 @@ func outputResult(p *print.Printer, outputFormat string, backup postgresflex.Bac } backupExpireDate := backupStartTime.AddDate(backupExpireYearOffset, backupExpireMonthOffset, backupExpireDayOffset).Format(time.DateOnly) - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backup, "", " ") - if err != nil { - return fmt.Errorf("marshal backup for PostgreSQL Flex backup: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal backup for PostgreSQL Flex backup: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, backup, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(backup.Id)) table.AddSeparator() @@ -152,5 +138,5 @@ func outputResult(p *print.Printer, outputFormat string, backup postgresflex.Bac } return nil - } + }) } diff --git a/internal/cmd/postgresflex/backup/describe/describe_test.go b/internal/cmd/postgresflex/backup/describe/describe_test.go index 6e47eabb0..a77f0fed5 100644 --- a/internal/cmd/postgresflex/backup/describe/describe_test.go +++ b/internal/cmd/postgresflex/backup/describe/describe_test.go @@ -8,17 +8,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testBackupId = "backupID" @@ -63,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiGetBackupRequest)) postgresflex.ApiGetBackupRequest { - request := testClient.GetBackup(testCtx, testProjectId, testRegion, testInstanceId, testBackupId) + request := testClient.DefaultAPI.GetBackup(testCtx, testProjectId, testRegion, testInstanceId, testBackupId) for _, mod := range mods { mod(&request) } @@ -231,7 +231,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -243,31 +243,52 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string - backup postgresflex.Backup + backup *postgresflex.Backup } tests := []struct { name string args args wantErr bool }{ - {"empty", args{}, true}, - {"standard", args{outputFormat: "", backup: postgresflex.Backup{StartTime: utils.Ptr(time.Now().Format(time.RFC3339))}}, false}, - {"complete", args{outputFormat: "", backup: postgresflex.Backup{ - EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), - Id: utils.Ptr("id"), - Labels: &[]string{"foo", "bar", "baz"}, - Name: utils.Ptr("name"), - Options: &map[string]string{"test1": "test1", "test2": "test2"}, - Size: utils.Ptr(int64(42)), - StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), - }}, false}, + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "standard", + args: args{ + outputFormat: "", + backup: &postgresflex.Backup{ + StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), + }, + }, + wantErr: false, + }, + { + name: "complete", + args: args{ + outputFormat: "", + backup: &postgresflex.Backup{ + EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), + Id: utils.Ptr("id"), + Labels: []string{"foo", + "bar", + "baz", + }, + Name: utils.Ptr("name"), + Options: &map[string]string{"test1": "test1", "test2": "test2"}, + Size: utils.Ptr(int64(42)), + StartTime: utils.Ptr(time.Now().Format(time.RFC3339)), + }, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/backup/list/list.go b/internal/cmd/postgresflex/backup/list/list.go index 5527343ad..0b4d763e9 100644 --- a/internal/cmd/postgresflex/backup/list/list.go +++ b/internal/cmd/postgresflex/backup/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,7 +20,7 @@ import ( "time" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) const ( @@ -39,7 +38,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all backups which are available for a PostgreSQL Flex instance", @@ -56,9 +55,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit postgresflex backup list --instance-id xxx --limit 10"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,7 +68,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, *model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = *model.InstanceId @@ -79,20 +78,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() if err != nil { - return fmt.Errorf("get backups for PostgreSQL Flex instance %q: %w\n", instanceLabel, err) - } - if resp.Items == nil || len(*resp.Items) == 0 { - cmd.Printf("No backups found for instance %q\n", instanceLabel) - return nil + return fmt.Errorf("get backups for PostgreSQL Flex instance %q: %w", instanceLabel, err) } - backups := *resp.Items + backups := resp.Items // Truncate output if model.Limit != nil && len(backups) > int(*model.Limit) { backups = backups[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, backups) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, backups) }, } @@ -108,7 +103,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -130,29 +125,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiListBackupsRequest { - req := apiClient.ListBackups(ctx, model.ProjectId, model.Region, *model.InstanceId) + req := apiClient.DefaultAPI.ListBackups(ctx, model.ProjectId, model.Region, *model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, backups []postgresflex.Backup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backups, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex backup list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex backup list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, backups []postgresflex.Backup) error { + return p.OutputResult(outputFormat, backups, func() error { + if len(backups) == 0 { + p.Outputf("No backups found for instance %q", instanceLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "CREATED AT", "EXPIRES AT", "BACKUP SIZE") for i := range backups { @@ -178,5 +160,5 @@ func outputResult(p *print.Printer, outputFormat string, backups []postgresflex. } return nil - } + }) } diff --git a/internal/cmd/postgresflex/backup/list/list_test.go b/internal/cmd/postgresflex/backup/list/list_test.go index 9dee772fe..34177acb8 100644 --- a/internal/cmd/postgresflex/backup/list/list_test.go +++ b/internal/cmd/postgresflex/backup/list/list_test.go @@ -5,20 +5,23 @@ import ( "testing" "time" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -53,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiListBackupsRequest)) postgresflex.ApiListBackupsRequest { - request := testClient.ListBackups(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.ListBackups(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -63,6 +66,7 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiListBackupsRequest)) p func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -138,45 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -211,8 +177,9 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { - outputFormat string - backups []postgresflex.Backup + outputFormat string + instanceLabel string + backups []postgresflex.Backup } tests := []struct { name string @@ -220,12 +187,12 @@ func Test_outputResult(t *testing.T) { wantErr bool }{ {"empty", args{}, false}, - {"standard", args{outputFormat: "", backups: []postgresflex.Backup{}}, false}, - {"complete", args{outputFormat: "", backups: []postgresflex.Backup{ + {"standard", args{outputFormat: "", instanceLabel: "label", backups: []postgresflex.Backup{}}, false}, + {"complete", args{outputFormat: "", instanceLabel: "label", backups: []postgresflex.Backup{ { EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), Id: utils.Ptr("id"), - Labels: &[]string{"foo", "bar", "baz"}, + Labels: []string{"foo", "bar", "baz"}, Name: utils.Ptr("name"), Options: &map[string]string{"test1": "test1", "test2": "test2"}, Size: utils.Ptr(int64(42)), @@ -234,7 +201,7 @@ func Test_outputResult(t *testing.T) { { EndTime: utils.Ptr(time.Now().Format(time.RFC3339)), Id: utils.Ptr("id"), - Labels: &[]string{"foo", "bar", "baz"}, + Labels: []string{"foo", "bar", "baz"}, Name: utils.Ptr("name"), Options: &map[string]string{"test1": "test1", "test2": "test2"}, Size: utils.Ptr(int64(42)), @@ -243,12 +210,10 @@ func Test_outputResult(t *testing.T) { }, }, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backups); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.backups); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go b/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go index 376b04bae..8b200f83a 100644 --- a/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go +++ b/internal/cmd/postgresflex/backup/update-schedule/update_schedule.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( @@ -25,11 +27,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - InstanceId *string - BackupSchedule *string + InstanceId string + BackupSchedule string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "update-schedule", Short: "Updates backup schedule for a PostgreSQL Flex instance", @@ -41,10 +43,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit postgresflex backup update-schedule --instance-id xxx --schedule '6 6 * * *'"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -55,18 +57,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, *model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = *model.InstanceId + instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -92,7 +92,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -100,13 +100,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return &inputModel{ GlobalFlagModel: globalFlags, - InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), - BackupSchedule: flags.FlagToStringPointer(p, cmd, scheduleFlag), + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + BackupSchedule: flags.FlagToStringValue(p, cmd, scheduleFlag), }, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiUpdateBackupScheduleRequest { - req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, model.Region, *model.InstanceId) + req := apiClient.DefaultAPI.UpdateBackupSchedule(ctx, model.ProjectId, model.Region, model.InstanceId) req = req.UpdateBackupSchedulePayload(postgresflex.UpdateBackupSchedulePayload{ BackupSchedule: model.BackupSchedule, }) diff --git a/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go index 446f96226..93ef49667 100644 --- a/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go +++ b/internal/cmd/postgresflex/backup/update-schedule/update_schedule_test.go @@ -5,18 +5,18 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testSchedule = "0 0 * * *" @@ -42,8 +42,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceId: utils.Ptr(testInstanceId), - BackupSchedule: &testSchedule, + InstanceId: testInstanceId, + BackupSchedule: testSchedule, } for _, mod := range mods { mod(model) @@ -53,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixturePayload(mods ...func(payload *postgresflex.UpdateBackupSchedulePayload)) postgresflex.UpdateBackupSchedulePayload { payload := postgresflex.UpdateBackupSchedulePayload{ - BackupSchedule: utils.Ptr(testSchedule), + BackupSchedule: testSchedule, } for _, mod := range mods { mod(&payload) @@ -62,7 +62,7 @@ func fixturePayload(mods ...func(payload *postgresflex.UpdateBackupSchedulePaylo } func fixtureRequest(mods ...func(request *postgresflex.ApiUpdateBackupScheduleRequest)) postgresflex.ApiUpdateBackupScheduleRequest { - request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.UpdateBackupSchedule(testCtx, testProjectId, testRegion, testInstanceId) request = request.UpdateBackupSchedulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -73,6 +73,7 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiUpdateBackupScheduleRe func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -142,45 +143,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := NewCmd(nil) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(nil, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,9 +166,9 @@ func TestBuildRequest(t *testing.T) { ProjectId: testProjectId, Region: testRegion, }, - InstanceId: utils.Ptr(testInstanceId), + InstanceId: testInstanceId, }, - expectedRequest: testClient.UpdateBackupSchedule(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.UpdateBackupSchedule(testCtx, testProjectId, testRegion, testInstanceId). UpdateBackupSchedulePayload(postgresflex.UpdateBackupSchedulePayload{}), }, } @@ -216,7 +179,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/postgresflex/instance/clone/clone.go b/internal/cmd/postgresflex/instance/clone/clone.go index 2967fdbb3..b5bea934c 100644 --- a/internal/cmd/postgresflex/instance/clone/clone.go +++ b/internal/cmd/postgresflex/instance/clone/clone.go @@ -2,11 +2,10 @@ package clone import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" ) const ( @@ -41,7 +40,7 @@ type inputModel struct { RecoveryDate *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("clone %s", instanceIdArg), Short: "Clones a PostgreSQL Flex instance", @@ -73,22 +72,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to clone instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -100,13 +97,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Cloning instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Cloning instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for PostgreSQL Flex instance cloning: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, instanceId, resp) @@ -150,30 +147,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu RecoveryDate: utils.Ptr(recoveryTimestampString), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type PostgreSQLFlexClient interface { - CloneInstance(ctx context.Context, projectId, region, instanceId string) postgresflex.ApiCloneInstanceRequest - GetInstanceExecute(ctx context.Context, projectId, region, instanceId string) (*postgresflex.InstanceResponse, error) - ListStoragesExecute(ctx context.Context, projectId, region, flavorId string) (*postgresflex.ListStoragesResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (postgresflex.ApiCloneInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (postgresflex.ApiCloneInstanceRequest, error) { req := apiClient.CloneInstance(ctx, model.ProjectId, model.Region, model.InstanceId) var storages *postgresflex.ListStoragesResponse if model.StorageClass != nil || model.StorageSize != nil { - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.Region, model.InstanceId) + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) } @@ -181,7 +164,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFl currentInstanceStorageClass := currentInstance.Item.Storage.Class currentInstanceStorageSize := currentInstance.Item.Storage.Size - storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, model.Region, *validationFlavorId) + storages, err = apiClient.ListStorages(ctx, model.ProjectId, model.Region, *validationFlavorId).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) } @@ -207,32 +190,15 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFl } func outputResult(p *print.Printer, outputFormat string, async bool, instanceLabel, instanceId string, resp *postgresflex.CloneInstanceResponse) error { - if resp == nil { - return fmt.Errorf("response not set") - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgresFlex instance clone: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgresFlex instance clone: %w", err) + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("response not set") } - p.Outputln(string(details)) - - return nil - default: operationState := "Cloned" if async { operationState = "Triggered cloning of" } p.Info("%s instance from instance %q. New Instance ID: %s\n", operationState, instanceLabel, instanceId) return nil - } + }) } diff --git a/internal/cmd/postgresflex/instance/clone/clone_test.go b/internal/cmd/postgresflex/instance/clone/clone_test.go index 04dbd1954..4e99f08e7 100644 --- a/internal/cmd/postgresflex/instance/clone/clone_test.go +++ b/internal/cmd/postgresflex/instance/clone/clone_test.go @@ -9,41 +9,41 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} -type postgresFlexClientMocked struct { +type mockSettings struct { listStoragesFails bool listStoragesResp *postgresflex.ListStoragesResponse getInstanceFails bool getInstanceResp *postgresflex.InstanceResponse } -func (c *postgresFlexClientMocked) CloneInstance(ctx context.Context, projectId, region, instanceId string) postgresflex.ApiCloneInstanceRequest { - return testClient.CloneInstance(ctx, projectId, region, instanceId) -} - -func (c *postgresFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*postgresflex.InstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") - } - return c.getInstanceResp, nil -} - -func (c *postgresFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*postgresflex.ListStoragesResponse, error) { - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") +func newAPIMockClient(c mockSettings) postgresflex.DefaultAPI { + return &postgresflex.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { + if c.getInstanceFails { + return nil, fmt.Errorf("get instance failed") + } + return c.getInstanceResp, nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { + if c.listStoragesFails { + return nil, fmt.Errorf("list storages failed") + } + return c.listStoragesResp, nil + }), } - return c.listStoragesResp, nil } var testProjectId = uuid.NewString() @@ -137,7 +137,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiCloneInstanceRequest)) postgresflex.ApiCloneInstanceRequest { - request := testClient.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId) request = request.CloneInstancePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -302,54 +302,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -362,14 +315,11 @@ func TestBuildRequest(t *testing.T) { recoveryTimestampString := testRecoveryTimestamp.Format(recoveryDateFormat) tests := []struct { - description string - model *inputModel - expectedRequest postgresflex.ApiCloneInstanceRequest - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse - listStoragesFails bool - listStoragesResp *postgresflex.ListStoragesResponse - isValid bool + description string + model *inputModel + expectedRequest postgresflex.ApiCloneInstanceRequest + mockClientSettings mockSettings + isValid bool }{ { description: "base", @@ -383,25 +333,27 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("class") }), isValid: true, - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, + Storage: &postgresflex.Storage{ + Class: utils.Ptr(testStorageClass), + Size: utils.Ptr(testStorageSize), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, - expectedRequest: testClient.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId). CloneInstancePayload(postgresflex.CloneInstancePayload{ Class: utils.Ptr("class"), Timestamp: utils.Ptr(recoveryTimestampString), @@ -414,25 +366,27 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(10)) }), isValid: true, - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, + Storage: &postgresflex.Storage{ + Class: utils.Ptr(testStorageClass), + Size: utils.Ptr(testStorageSize), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, - expectedRequest: testClient.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.CloneInstance(testCtx, testProjectId, testRegion, testInstanceId). CloneInstancePayload(postgresflex.CloneInstancePayload{ Class: utils.Ptr("class"), Size: utils.Ptr(int64(10)), @@ -447,8 +401,10 @@ func TestBuildRequest(t *testing.T) { model.RecoveryDate = utils.Ptr(recoveryTimestampString) }, ), - getInstanceFails: true, - isValid: false, + mockClientSettings: mockSettings{ + getInstanceFails: true, + }, + isValid: false, }, { description: "invalid storage class", @@ -457,22 +413,24 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("non-existing-class") }, ), - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, + Storage: &postgresflex.Storage{ + Class: utils.Ptr(testStorageClass), + Size: utils.Ptr(testStorageSize), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -484,22 +442,24 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(9)) }, ), - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), - }, - Storage: &postgresflex.Storage{ - Class: utils.Ptr(testStorageClass), - Size: utils.Ptr(testStorageSize), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, + Storage: &postgresflex.Storage{ + Class: utils.Ptr(testStorageClass), + Size: utils.Ptr(testStorageSize), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -508,13 +468,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - getInstanceFails: tt.getInstanceFails, - getInstanceResp: tt.getInstanceResp, - listStoragesFails: tt.listStoragesFails, - listStoragesResp: tt.listStoragesResp, - } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMockClient(tt.mockClientSettings)) if err != nil { if !tt.isValid { return @@ -525,6 +479,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -553,12 +508,10 @@ func Test_outputResult(t *testing.T) { resp: &postgresflex.CloneInstanceResponse{InstanceId: utils.Ptr("id")}, }, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.OutputFormat, tt.args.async, tt.args.instanceLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.OutputFormat, tt.args.async, tt.args.instanceLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/instance/create/create.go b/internal/cmd/postgresflex/instance/create/create.go index 7def7878c..5537cc370 100644 --- a/internal/cmd/postgresflex/instance/create/create.go +++ b/internal/cmd/postgresflex/instance/create/create.go @@ -2,12 +2,11 @@ package create import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" ) const ( @@ -35,7 +34,6 @@ const ( storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" - typeFlag = "type" defaultBackupSchedule = "0 0 * * *" defaultStorageClass = "premium-perf2-stackit" @@ -43,22 +41,31 @@ const ( defaultType = "Replica" ) +var ( + typeFlag = flags.StringEnumFlag( + "type", + postgresflexUtils.AvailableInstanceTypes(), + "Instance type,", + flags.StringEnumDefaultValue(defaultType), + ) +) + type inputModel struct { *globalflags.GlobalFlagModel - InstanceName *string - ACL *[]string - BackupSchedule *string + InstanceName string + ACL []string + BackupSchedule string FlavorId *string CPU *int64 RAM *int64 StorageClass *string StorageSize *int64 - Version *string - Type *string + Version string + Type string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a PostgreSQL Flex instance", @@ -75,10 +82,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a PostgreSQL Flex instance with name "my-instance", allow access to a specific range of IP addresses, specify flavor by CPU and RAM and set storage size to 20 GB. Other parameters are set to default values`, `$ stackit postgresflex instance create --name my-instance --cpu 2 --ram 4 --acl 1.2.3.0/24 --storage-size 20`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -95,25 +102,23 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a PostgreSQL Flex instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a PostgreSQL Flex instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Fill in version, if needed - if model.Version == nil { - version, err := postgresflexUtils.GetLatestPostgreSQLVersion(ctx, apiClient, model.ProjectId, model.Region) + if model.Version == "" { + version, err := postgresflexUtils.GetLatestPostgreSQLVersion(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region) if err != nil { return fmt.Errorf("get latest PostgreSQL version: %w", err) } - model.Version = &version + model.Version = version } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -125,13 +130,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for PostgreSQL Flex instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, instanceId, resp) @@ -142,8 +147,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - typeFlagOptions := postgresflexUtils.AvailableInstanceTypes() - cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "The access control list (ACL). Must contain at least one valid subnet, for instance '0.0.0.0/0' for open access (discouraged), '1.2.3.0/24 for a public IP range of an organization, '1.2.3.4/32' for a single IP range, etc.") cmd.Flags().String(backupScheduleFlag, defaultBackupSchedule, "Backup schedule") @@ -153,13 +156,13 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(storageClassFlag, defaultStorageClass, "Storage class") cmd.Flags().Int64(storageSizeFlag, defaultStorageSize, "Storage size (in GB)") cmd.Flags().String(versionFlag, "", "PostgreSQL version. Defaults to the latest version available") - cmd.Flags().Var(flags.EnumFlag(false, defaultType, typeFlagOptions...), typeFlag, fmt.Sprintf("Instance type, one of %q", typeFlagOptions)) + typeFlag.Register(cmd.Flags()) err := flags.MarkFlagsRequired(cmd, instanceNameFlag, aclFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -184,49 +187,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), - ACL: flags.FlagToStringSlicePointer(p, cmd, aclFlag), - BackupSchedule: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, backupScheduleFlag)), + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), + ACL: flags.FlagToStringSliceValue(p, cmd, aclFlag), + BackupSchedule: flags.FlagWithDefaultToStringValue(p, cmd, backupScheduleFlag), FlavorId: flavorId, CPU: cpu, RAM: ram, StorageClass: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, storageClassFlag)), StorageSize: &storageSize, - Version: flags.FlagToStringPointer(p, cmd, versionFlag), - Type: utils.Ptr(flags.FlagWithDefaultToStringValue(p, cmd, typeFlag)), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Version: flags.FlagToStringValue(p, cmd, versionFlag), + Type: typeFlag.Get(), } + p.DebugInputModel(model) return &model, nil } -type PostgreSQLFlexClient interface { - CreateInstance(ctx context.Context, projectId, region string) postgresflex.ApiCreateInstanceRequest - ListFlavorsExecute(ctx context.Context, projectId, region string) (*postgresflex.ListFlavorsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, region, flavorId string) (*postgresflex.ListStoragesResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (postgresflex.ApiCreateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (postgresflex.ApiCreateInstanceRequest, error) { req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) - var flavorId *string + var flavorId string var err error - flavors, err := apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex flavors: %w", err) } if model.FlavorId == nil { - flavorId, err = postgresflexUtils.LoadFlavorId(*model.CPU, *model.RAM, flavors.Flavors) + foundFlavorId, err := postgresflexUtils.LoadFlavorId(*model.CPU, *model.RAM, flavors.Flavors) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -234,73 +223,57 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFl } return req, err } + flavorId = *foundFlavorId } else { err := postgresflexUtils.ValidateFlavorId(*model.FlavorId, flavors.Flavors) if err != nil { return req, err } - flavorId = model.FlavorId + flavorId = *model.FlavorId } - storages, err := apiClient.ListStoragesExecute(ctx, model.ProjectId, model.Region, *flavorId) + storages, err := apiClient.ListStorages(ctx, model.ProjectId, model.Region, flavorId).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) } - err = postgresflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, *flavorId) + err = postgresflexUtils.ValidateStorage(model.StorageClass, model.StorageSize, storages, flavorId) if err != nil { return req, err } - replicas, err := postgresflexUtils.GetInstanceReplicas(*model.Type) + replicas, err := postgresflexUtils.GetInstanceReplicas(model.Type) if err != nil { return req, fmt.Errorf("get PostgreSQL Flex instance type: %w", err) } req = req.CreateInstancePayload(postgresflex.CreateInstancePayload{ Name: model.InstanceName, - Acl: &postgresflex.ACL{Items: model.ACL}, + Acl: postgresflex.ACL{Items: model.ACL}, BackupSchedule: model.BackupSchedule, FlavorId: flavorId, - Replicas: &replicas, - Storage: &postgresflex.Storage{ + Replicas: replicas, + Storage: postgresflex.Storage{ Class: model.StorageClass, Size: model.StorageSize, }, Version: model.Version, - Options: utils.Ptr(map[string]string{ - "type": *model.Type, - }), + Options: map[string]string{ + "type": model.Type, + }, }) return req, nil } func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel, instanceId string, resp *postgresflex.CreateInstanceResponse) error { - if resp == nil { - return fmt.Errorf("no response passed") - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgresFlex instance: %w", err) + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("no response passed") } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgresFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: operationState := "Created" if async { operationState = "Triggered creation of" } p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, instanceId) return nil - } + }) } diff --git a/internal/cmd/postgresflex/instance/create/create_test.go b/internal/cmd/postgresflex/instance/create/create_test.go index 32d63d5d8..d0f86d364 100644 --- a/internal/cmd/postgresflex/instance/create/create_test.go +++ b/internal/cmd/postgresflex/instance/create/create_test.go @@ -8,42 +8,42 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testRegion = "eu01" -type postgresFlexClientMocked struct { +type mockSettings struct { listFlavorsFails bool listFlavorsResp *postgresflex.ListFlavorsResponse listStoragesFails bool listStoragesResp *postgresflex.ListStoragesResponse } -func (c *postgresFlexClientMocked) CreateInstance(ctx context.Context, projectId, region string) postgresflex.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId, region) -} - -func (c *postgresFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*postgresflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil -} - -func (c *postgresFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*postgresflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") +func newAPIClientMock(c mockSettings) postgresflex.DefaultAPI { + return postgresflex.DefaultAPIServiceMock{ + ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { + if c.listStoragesFails { + return nil, fmt.Errorf("list storages failed") + } + return c.listStoragesResp, nil + }), + ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { + if c.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return c.listFlavorsResp, nil + }), } - return c.listFlavorsResp, nil } var testProjectId = uuid.NewString() @@ -60,7 +60,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st storageClassFlag: "premium-perf4-stackit", // Non-default storageSizeFlag: "10", versionFlag: "6.0", - typeFlag: "Replica", + typeFlag.Name(): "Replica", } for _, mod := range mods { mod(flagValues) @@ -75,14 +75,14 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), - ACL: utils.Ptr([]string{"0.0.0.0/0"}), - BackupSchedule: utils.Ptr("0 0 * * *"), + InstanceName: "example-name", + ACL: []string{"0.0.0.0/0"}, + BackupSchedule: "0 0 * * *", FlavorId: utils.Ptr(testFlavorId), StorageClass: utils.Ptr("premium-perf4-stackit"), StorageSize: utils.Ptr(int64(10)), - Version: utils.Ptr("6.0"), - Type: utils.Ptr("Replica"), + Version: "6.0", + Type: "Replica", } for _, mod := range mods { mod(model) @@ -91,7 +91,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiCreateInstanceRequest)) postgresflex.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion) request = request.CreateInstancePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -101,19 +101,19 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiCreateInstanceRequest) func fixturePayload(mods ...func(payload *postgresflex.CreateInstancePayload)) postgresflex.CreateInstancePayload { payload := postgresflex.CreateInstancePayload{ - Name: utils.Ptr("example-name"), - Acl: &postgresflex.ACL{Items: utils.Ptr([]string{"0.0.0.0/0"})}, - BackupSchedule: utils.Ptr("0 0 * * *"), - FlavorId: utils.Ptr(testFlavorId), - Replicas: utils.Ptr(int64(3)), - Storage: &postgresflex.Storage{ + Name: "example-name", + Acl: postgresflex.ACL{Items: []string{"0.0.0.0/0"}}, + BackupSchedule: "0 0 * * *", + FlavorId: testFlavorId, + Replicas: int32(3), + Storage: postgresflex.Storage{ Class: utils.Ptr("premium-perf4-stackit"), Size: utils.Ptr(int64(10)), }, - Version: utils.Ptr("6.0"), - Options: utils.Ptr(map[string]string{ + Version: "6.0", + Options: map[string]string{ "type": "Replica", - }), + }, } for _, mod := range mods { mod(&payload) @@ -124,6 +124,7 @@ func fixturePayload(mods ...func(payload *postgresflex.CreateInstancePayload)) p func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -139,7 +140,7 @@ func TestParseInput(t *testing.T) { description: "with defaults", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, backupScheduleFlag) - delete(flagValues, typeFlag) + delete(flagValues, typeFlag.Name()) }), isValid: true, expectedModel: fixtureInputModel(), @@ -213,7 +214,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Version = nil + model.Version = "" }), }, { @@ -222,9 +223,8 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.ACL = utils.Ptr( - append(*model.ACL, "198.51.100.14/24", "198.51.100.14/32"), - ) + model.ACL = + append(model.ACL, "198.51.100.14/24", "198.51.100.14/32") }), }, { @@ -233,9 +233,8 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24,198.51.100.14/32"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.ACL = utils.Ptr( - append(*model.ACL, "198.51.100.14/24", "198.51.100.14/32"), - ) + model.ACL = + append(model.ACL, "198.51.100.14/24", "198.51.100.14/32") }), }, { @@ -250,90 +249,42 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.aclValues { - err := cmd.Flags().Set(aclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", aclFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + aclFlag: tt.aclValues, + }, tt.isValid) }) } } func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest postgresflex.ApiCreateInstanceRequest - listFlavorsFails bool - listFlavorsResp *postgresflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *postgresflex.ListStoragesResponse - isValid bool + description string + model *inputModel + expectedRequest postgresflex.ApiCreateInstanceRequest + mockClientSettings mockSettings + isValid bool }{ { description: "base with flavor ID", model: fixtureInputModel(), isValid: true, expectedRequest: fixtureRequest(), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-stackit"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, @@ -348,50 +299,54 @@ func TestBuildRequest(t *testing.T) { ), isValid: true, expectedRequest: fixtureRequest(), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, + { + Id: utils.Ptr("other-flavor"), + Cpu: utils.Ptr(int64(1)), + Memory: utils.Ptr(int64(8)), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-stackit"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, { description: "single instance type", - model: fixtureInputModel(func(model *inputModel) { model.Type = utils.Ptr("Single") }), + model: fixtureInputModel(func(model *inputModel) { model.Type = "Single" }), isValid: true, expectedRequest: fixtureRequest().CreateInstancePayload(fixturePayload(func(payload *postgresflex.CreateInstancePayload) { - payload.Options = utils.Ptr(map[string]string{"type": "Single"}) - payload.Replicas = utils.Ptr(int64(1)) + payload.Options = map[string]string{"type": "Single"} + payload.Replicas = int32(1) })), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-stackit"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, }, @@ -404,8 +359,10 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "flavor id not found", @@ -416,17 +373,19 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(9)) }, ), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, + { + Id: utils.Ptr("other-flavor"), + Cpu: utils.Ptr(int64(1)), + Memory: utils.Ptr(int64(8)), + }, }, }, }, @@ -441,8 +400,10 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "invalid storage class", @@ -451,20 +412,22 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("non-existing-class") }, ), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-stackit"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -476,20 +439,22 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(9)) }, ), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"premium-perf4-stackit"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"premium-perf4-stackit"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -498,13 +463,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - listFlavorsFails: tt.listFlavorsFails, - listFlavorsResp: tt.listFlavorsResp, - listStoragesFails: tt.listStoragesFails, - listStoragesResp: tt.listStoragesResp, - } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIClientMock(tt.mockClientSettings)) if err != nil { if !tt.isValid { return @@ -515,6 +474,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -547,11 +507,10 @@ func Test_outputResult(t *testing.T) { false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/instance/delete/delete.go b/internal/cmd/postgresflex/instance/delete/delete.go index 1c892657d..c26787a42 100644 --- a/internal/cmd/postgresflex/instance/delete/delete.go +++ b/internal/cmd/postgresflex/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" ) const ( @@ -33,7 +34,7 @@ type inputModel struct { ForceDelete bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a PostgreSQL Flex instance", @@ -64,21 +65,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } - toDelete, toForceDelete, err := getNextOperations(ctx, model, apiClient) + toDelete, toForceDelete, err := getNextOperations(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -93,13 +92,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for PostgreSQL Flex instance deletion: %w", err) } - s.Stop() } } @@ -113,13 +112,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Forcing deletion of instance") - _, err = wait.ForceDeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Forcing deletion of instance", func() error { + _, err = wait.ForceDeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for PostgreSQL Flex instance force deletion: %w", err) } - s.Stop() } } @@ -159,35 +158,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ForceDelete: flags.FlagToBoolValue(p, cmd, forceDeleteFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildDeleteRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } func buildForceDeleteRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiForceDeleteInstanceRequest { - req := apiClient.ForceDeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) + req := apiClient.DefaultAPI.ForceDeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } -type PostgreSQLFlexClient interface { - GetInstanceExecute(ctx context.Context, projectId, region, instanceId string) (*postgresflex.InstanceResponse, error) - ListVersionsExecute(ctx context.Context, projectId, region string) (*postgresflex.ListVersionsResponse, error) - GetUserExecute(ctx context.Context, projectId, region, instanceId, userId string) (*postgresflex.GetUserResponse, error) -} - -func getNextOperations(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (toDelete, toForceDelete bool, err error) { +func getNextOperations(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (toDelete, toForceDelete bool, err error) { instanceStatus, err := postgresflexUtils.GetInstanceStatus(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) if err != nil { return false, false, fmt.Errorf("get PostgreSQL Flex instance status: %w", err) diff --git a/internal/cmd/postgresflex/instance/delete/delete_test.go b/internal/cmd/postgresflex/instance/delete/delete_test.go index 13f33bfbd..ffdd8c3b9 100644 --- a/internal/cmd/postgresflex/instance/delete/delete_test.go +++ b/internal/cmd/postgresflex/instance/delete/delete_test.go @@ -5,45 +5,39 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" -type postgresFlexClientMocked struct { +type mockSettings struct { getInstanceFails bool getInstanceResp *postgresflex.InstanceResponse } -func (c *postgresFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*postgresflex.InstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") +func newAPIMockClient(c mockSettings) postgresflex.DefaultAPI { + return postgresflex.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { + if c.getInstanceFails { + return nil, fmt.Errorf("get instance failed") + } + return c.getInstanceResp, nil + }), } - return c.getInstanceResp, nil -} - -func (c *postgresFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*postgresflex.ListVersionsResponse, error) { - // Not used in testing - return nil, nil -} -func (c *postgresFlexClientMocked) GetUserExecute(_ context.Context, _, _, _, _ string) (*postgresflex.GetUserResponse, error) { - // Not used in testing - return nil, nil } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -83,7 +77,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureDeleteRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRequest)) postgresflex.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -91,7 +85,7 @@ func fixtureDeleteRequest(mods ...func(request *postgresflex.ApiDeleteInstanceRe } func fixtureForceDeleteRequest(mods ...func(request *postgresflex.ApiForceDeleteInstanceRequest)) postgresflex.ApiForceDeleteInstanceRequest { - request := testClient.ForceDeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.ForceDeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -171,54 +165,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -242,7 +189,7 @@ func TestBuildDeleteRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -270,7 +217,7 @@ func TestBuildForceDeleteRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -285,8 +232,7 @@ func TestCheckIfInstanceIsDeleted(t *testing.T) { model *inputModel expectedToDelete bool expectedToForceDelete bool - getInstanceResponse *postgresflex.InstanceResponse - getInstanceFails bool + mockClientSettings mockSettings isValid bool }{ { @@ -294,9 +240,11 @@ func TestCheckIfInstanceIsDeleted(t *testing.T) { model: fixtureInputModel(), expectedToDelete: true, expectedToForceDelete: false, - getInstanceResponse: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateSuccess), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateSuccess), + }, }, }, isValid: true, @@ -308,9 +256,11 @@ func TestCheckIfInstanceIsDeleted(t *testing.T) { }), expectedToDelete: true, expectedToForceDelete: true, - getInstanceResponse: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateSuccess), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateSuccess), + }, }, }, isValid: true, @@ -322,9 +272,11 @@ func TestCheckIfInstanceIsDeleted(t *testing.T) { }), expectedToDelete: false, expectedToForceDelete: true, - getInstanceResponse: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateDeleted), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateDeleted), + }, }, }, isValid: true, @@ -332,29 +284,28 @@ func TestCheckIfInstanceIsDeleted(t *testing.T) { { description: "delete instance state Deleted", model: fixtureInputModel(), - getInstanceResponse: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(wait.InstanceStateDeleted), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(wait.InstanceStateDeleted), + }, }, }, isValid: false, }, { - description: "delete instance get instance fails", - model: fixtureInputModel(), - getInstanceFails: true, - isValid: false, + description: "delete instance get instance fails", + model: fixtureInputModel(), + mockClientSettings: mockSettings{ + getInstanceFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - getInstanceResp: tt.getInstanceResponse, - getInstanceFails: tt.getInstanceFails, - } - - toDelete, toForceDelete, err := getNextOperations(testCtx, tt.model, client) + toDelete, toForceDelete, err := getNextOperations(testCtx, tt.model, newAPIMockClient(tt.mockClientSettings)) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/postgresflex/instance/describe/describe.go b/internal/cmd/postgresflex/instance/describe/describe.go index 9d58c781b..8a3b277c4 100644 --- a/internal/cmd/postgresflex/instance/describe/describe.go +++ b/internal/cmd/postgresflex/instance/describe/describe.go @@ -2,11 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,11 +18,9 @@ import ( postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "golang.org/x/text/cases" - "golang.org/x/text/language" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) const ( @@ -32,7 +32,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a PostgreSQL Flex instance", @@ -84,48 +84,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } func outputResult(p *print.Printer, outputFormat string, instance *postgresflex.Instance) error { - if instance == nil { - return fmt.Errorf("no response passed") - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance: %w", err) + return p.OutputResult(outputFormat, instance, func() error { + if instance == nil { + return fmt.Errorf("no response passed") } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: acls := "" if instance.HasAcl() && instance.Acl.HasItems() { - acls = utils.JoinStringPtr(instance.Acl.Items, ",") + acls = utils.JoinStringPtr(&instance.Acl.Items, ",") } instanceType, err := postgresflexUtils.GetInstanceType(utils.PtrValue(instance.Replicas)) @@ -171,5 +146,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *postgresflex. } return nil - } + }) } diff --git a/internal/cmd/postgresflex/instance/describe/describe_test.go b/internal/cmd/postgresflex/instance/describe/describe_test.go index 69e100822..487343136 100644 --- a/internal/cmd/postgresflex/instance/describe/describe_test.go +++ b/internal/cmd/postgresflex/instance/describe/describe_test.go @@ -7,16 +7,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -58,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiGetInstanceRequest)) postgresflex.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -237,7 +191,7 @@ func Test_outputResult(t *testing.T) { outputFormat: "", instance: &postgresflex.Instance{ Acl: &postgresflex.ACL{ - Items: &[]string{}, + Items: []string{}, }, BackupSchedule: new(string), Flavor: &postgresflex.Flavor{ @@ -249,7 +203,7 @@ func Test_outputResult(t *testing.T) { Id: new(string), Name: new(string), Options: &map[string]string{}, - Replicas: new(int64), + Replicas: new(int32), Status: new(string), Storage: &postgresflex.Storage{ Class: new(string), @@ -259,12 +213,10 @@ func Test_outputResult(t *testing.T) { }, }, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/instance/instance.go b/internal/cmd/postgresflex/instance/instance.go index f43979195..5beba3e48 100644 --- a/internal/cmd/postgresflex/instance/instance.go +++ b/internal/cmd/postgresflex/instance/instance.go @@ -1,7 +1,6 @@ package instance import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance/clone" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance/create" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance/delete" @@ -9,12 +8,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for PostgreSQL Flex instances", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/postgresflex/instance/list/list.go b/internal/cmd/postgresflex/instance/list/list.go index 2431afa89..ac3eac5a7 100644 --- a/internal/cmd/postgresflex/instance/list/list.go +++ b/internal/cmd/postgresflex/instance/list/list.go @@ -2,12 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,9 +21,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) const ( @@ -32,7 +32,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all PostgreSQL Flex instances", @@ -49,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 PostgreSQL Flex instances`, "$ stackit postgresflex instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,23 +68,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get PostgreSQL Flex instances: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil - } - instances := *resp.Items + + instances := resp.Items // Truncate output if model.Limit != nil && len(instances) > int(*model.Limit) { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -96,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -115,42 +112,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, instances []postgresflex.InstanceListInstance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []postgresflex.InstanceListInstance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: caser := cases.Title(language.English) table := tables.NewTable() table.SetHeader("ID", "NAME", "STATUS") @@ -168,5 +144,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []postgresfle } return nil - } + }) } diff --git a/internal/cmd/postgresflex/instance/list/list_test.go b/internal/cmd/postgresflex/instance/list/list_test.go index 3cfc85652..9e08559f7 100644 --- a/internal/cmd/postgresflex/instance/list/list_test.go +++ b/internal/cmd/postgresflex/instance/list/list_test.go @@ -7,18 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testRegion = "eu01" @@ -50,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiListInstancesRequest)) postgresflex.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -60,6 +60,7 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiListInstancesRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -114,48 +115,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -179,7 +139,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -191,6 +151,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []postgresflex.InstanceListInstance } tests := []struct { @@ -199,8 +160,8 @@ func Test_outputResult(t *testing.T) { wantErr bool }{ {"empty", args{}, false}, - {"standard", args{"", []postgresflex.InstanceListInstance{}}, false}, - {"complete", args{"", []postgresflex.InstanceListInstance{ + {"standard", args{"", "label", []postgresflex.InstanceListInstance{}}, false}, + {"complete", args{"", "label", []postgresflex.InstanceListInstance{ { Id: new(string), Name: new(string), @@ -218,12 +179,10 @@ func Test_outputResult(t *testing.T) { }, }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/instance/update/update.go b/internal/cmd/postgresflex/instance/update/update.go index 3a50f8dfa..aaeedd410 100644 --- a/internal/cmd/postgresflex/instance/update/update.go +++ b/internal/cmd/postgresflex/instance/update/update.go @@ -2,12 +2,11 @@ package update import ( "context" - "encoding/json" "errors" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,8 +19,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api/wait" ) const ( @@ -36,7 +35,12 @@ const ( storageClassFlag = "storage-class" storageSizeFlag = "storage-size" versionFlag = "version" - typeFlag = "type" +) + +var typeFlag = flags.StringEnumFlag( + "type", + postgresflexUtils.AvailableInstanceTypes(), + "Instance type,", ) type inputModel struct { @@ -44,7 +48,7 @@ type inputModel struct { InstanceId string InstanceName *string - ACL *[]string + ACL []string BackupSchedule *string FlavorId *string CPU *int64 @@ -55,7 +59,7 @@ type inputModel struct { Type *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a PostgreSQL Flex instance", @@ -83,22 +87,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q? (This may cause downtime)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q? (This may cause downtime)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { return err } @@ -110,13 +112,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for PostgreSQL Flex instance update: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, instanceLabel, resp) @@ -127,8 +129,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - typeFlagOptions := postgresflexUtils.AvailableInstanceTypes() - cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().String(backupScheduleFlag, "", "Backup schedule") @@ -138,7 +138,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(storageClassFlag, "", "Storage class") cmd.Flags().Int64(storageSizeFlag, 0, "Storage size (in GB)") cmd.Flags().String(versionFlag, "", "Version") - cmd.Flags().Var(flags.EnumFlag(false, "", typeFlagOptions...), typeFlag, fmt.Sprintf("Instance type, one of %q", typeFlagOptions)) + typeFlag.Register(cmd.Flags()) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -153,12 +153,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu flavorId := flags.FlagToStringPointer(p, cmd, flavorIdFlag) cpu := flags.FlagToInt64Pointer(p, cmd, cpuFlag) ram := flags.FlagToInt64Pointer(p, cmd, ramFlag) - acl := flags.FlagToStringSlicePointer(p, cmd, aclFlag) + acl := flags.FlagToStringSliceValue(p, cmd, aclFlag) backupSchedule := flags.FlagToStringPointer(p, cmd, backupScheduleFlag) storageClass := flags.FlagToStringPointer(p, cmd, storageClassFlag) storageSize := flags.FlagToInt64Pointer(p, cmd, storageSizeFlag) version := flags.FlagToStringPointer(p, cmd, versionFlag) - instanceType := flags.FlagToStringPointer(p, cmd, typeFlag) + instanceType := typeFlag.Ptr() if instanceName == nil && flavorId == nil && cpu == nil && ram == nil && acl == nil && backupSchedule == nil && storageClass == nil && storageSize == nil && version == nil && instanceType == nil { @@ -187,32 +187,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Type: instanceType, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type PostgreSQLFlexClient interface { - PartialUpdateInstance(ctx context.Context, projectId, region, instanceId string) postgresflex.ApiPartialUpdateInstanceRequest - GetInstanceExecute(ctx context.Context, projectId, region, instanceId string) (*postgresflex.InstanceResponse, error) - ListFlavorsExecute(ctx context.Context, projectId, region string) (*postgresflex.ListFlavorsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, region, flavorId string) (*postgresflex.ListStoragesResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFlexClient) (postgresflex.ApiPartialUpdateInstanceRequest, error) { +func buildRequest(ctx context.Context, model *inputModel, apiClient postgresflex.DefaultAPI) (postgresflex.ApiPartialUpdateInstanceRequest, error) { req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.Region, model.InstanceId) var flavorId *string var err error - flavors, err := apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex flavors: %w", err) } @@ -221,7 +206,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFl ram := model.RAM cpu := model.CPU if model.RAM == nil || model.CPU == nil { - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.Region, model.InstanceId) + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) } @@ -252,13 +237,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFl if model.StorageClass != nil || model.StorageSize != nil { validationFlavorId := flavorId if validationFlavorId == nil { - currentInstance, err := apiClient.GetInstanceExecute(ctx, model.ProjectId, model.Region, model.InstanceId) + currentInstance, err := apiClient.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex instance: %w", err) } validationFlavorId = currentInstance.Item.Flavor.Id } - storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, model.Region, *validationFlavorId) + storages, err = apiClient.ListStorages(ctx, model.ProjectId, model.Region, *validationFlavorId).Execute() if err != nil { return req, fmt.Errorf("get PostgreSQL Flex storages: %w", err) } @@ -273,15 +258,15 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient PostgreSQLFl payloadAcl = &postgresflex.ACL{Items: model.ACL} } - var payloadStorage *postgresflex.Storage + var payloadStorage *postgresflex.StorageUpdate if model.StorageClass != nil || model.StorageSize != nil { - payloadStorage = &postgresflex.Storage{ + payloadStorage = &postgresflex.StorageUpdate{ Class: model.StorageClass, Size: model.StorageSize, } } - var replicas *int64 + var replicas *int32 var payloadOptions *map[string]string if model.Type != nil { replicasInt, err := postgresflexUtils.GetInstanceReplicas(*model.Type) @@ -313,29 +298,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, instanceLab return fmt.Errorf("no response passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgresFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgresFlex instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { operationState := "Updated" if async { operationState = "Triggered update of" } p.Info("%s instance %q\n", operationState, instanceLabel) return nil - } + }) } diff --git a/internal/cmd/postgresflex/instance/update/update_test.go b/internal/cmd/postgresflex/instance/update/update_test.go index 0c4ed96b6..1cb0b5aff 100644 --- a/internal/cmd/postgresflex/instance/update/update_test.go +++ b/internal/cmd/postgresflex/instance/update/update_test.go @@ -8,51 +8,49 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testRegion = "eu01" -type postgresFlexClientMocked struct { - listFlavorsFails bool - listFlavorsResp *postgresflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *postgresflex.ListStoragesResponse - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse -} - -func (c *postgresFlexClientMocked) PartialUpdateInstance(ctx context.Context, projectId, region, instanceId string) postgresflex.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, region, instanceId) +type mockSettings struct { + listFlavorsFails bool + listFlavorsResp *postgresflex.ListFlavorsResponse + listStoragesResp *postgresflex.ListStoragesResponse + getInstanceFails bool + getInstanceResp *postgresflex.InstanceResponse } -func (c *postgresFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*postgresflex.InstanceResponse, error) { - if c.getInstanceFails { - return nil, fmt.Errorf("get instance failed") - } - return c.getInstanceResp, nil -} - -func (c *postgresFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*postgresflex.ListStoragesResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list storages failed") - } - return c.listStoragesResp, nil -} - -func (c *postgresFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*postgresflex.ListFlavorsResponse, error) { - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") +func newAPIClientMock(c mockSettings) postgresflex.DefaultAPI { + return postgresflex.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { + if c.getInstanceFails { + return nil, fmt.Errorf("get instance failed") + } + return c.getInstanceResp, nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { + if c.listFlavorsFails { + return nil, fmt.Errorf("list storages failed") + } + return c.listStoragesResp, nil + }), + ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { + if c.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return c.listFlavorsResp, nil + }), } - return c.listFlavorsResp, nil } var testProjectId = uuid.NewString() @@ -91,7 +89,7 @@ func fixtureStandardFlagValues(mods ...func(flagValues map[string]string)) map[s storageClassFlag: "class", storageSizeFlag: "10", versionFlag: "5.0", - typeFlag: "Single", + typeFlag.Name(): "Single", } for _, mod := range mods { mod(flagValues) @@ -124,7 +122,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { InstanceId: testInstanceId, FlavorId: utils.Ptr(testFlavorId), InstanceName: utils.Ptr("example-name"), - ACL: utils.Ptr([]string{"0.0.0.0/0"}), + ACL: []string{"0.0.0.0/0"}, BackupSchedule: utils.Ptr("0 0 * * *"), StorageClass: utils.Ptr("class"), StorageSize: utils.Ptr(int64(10)), @@ -138,7 +136,7 @@ func fixtureStandardInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiPartialUpdateInstanceRequest)) postgresflex.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId) request = request.PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{}) for _, mod := range mods { mod(&request) @@ -275,15 +273,16 @@ func TestParseInput(t *testing.T) { aclValues: []string{"198.51.100.14/24", "198.51.100.14/32"}, isValid: true, expectedModel: fixtureRequiredInputModel(func(model *inputModel) { - model.ACL = utils.Ptr([]string{"198.51.100.14/24", "198.51.100.14/32"}) + model.ACL = []string{"198.51.100.14/24", "198.51.100.14/32"} }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + typeFlag.Reset() + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -325,7 +324,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -346,16 +345,11 @@ func TestParseInput(t *testing.T) { func TestBuildRequest(t *testing.T) { tests := []struct { - description string - model *inputModel - expectedRequest postgresflex.ApiPartialUpdateInstanceRequest - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse - listFlavorsFails bool - listFlavorsResp *postgresflex.ListFlavorsResponse - listStoragesFails bool - listStoragesResp *postgresflex.ListStoragesResponse - isValid bool + description string + model *inputModel + expectedRequest postgresflex.ApiPartialUpdateInstanceRequest + mockClientSettings mockSettings + isValid bool }{ { description: "no values", @@ -369,16 +363,18 @@ func TestBuildRequest(t *testing.T) { model.FlavorId = utils.Ptr(testFlavorId) }), isValid: true, - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ FlavorId: utils.Ptr(testFlavorId), }), @@ -390,16 +386,18 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(4)) }), isValid: true, - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ FlavorId: utils.Ptr(testFlavorId), }), @@ -410,23 +408,25 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("class") }), isValid: true, - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ - Storage: &postgresflex.Storage{ + Storage: &postgresflex.StorageUpdate{ Class: utils.Ptr("class"), }, }), @@ -438,23 +438,25 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(10)) }), isValid: true, - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(postgresflex.PartialUpdateInstancePayload{ - Storage: &postgresflex.Storage{ + Storage: &postgresflex.StorageUpdate{ Class: utils.Ptr("class"), Size: utils.Ptr(int64(10)), }, @@ -468,8 +470,10 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "flavor id not found", @@ -479,17 +483,19 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(9)) }, ), - listFlavorsResp: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{ - { - Id: utils.Ptr(testFlavorId), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), - }, - { - Id: utils.Ptr("other-flavor"), - Cpu: utils.Ptr(int64(1)), - Memory: utils.Ptr(int64(8)), + mockClientSettings: mockSettings{ + listFlavorsResp: &postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{ + { + Id: utils.Ptr(testFlavorId), + Cpu: utils.Ptr(int64(2)), + Memory: utils.Ptr(int64(4)), + }, + { + Id: utils.Ptr("other-flavor"), + Cpu: utils.Ptr(int64(1)), + Memory: utils.Ptr(int64(8)), + }, }, }, }, @@ -502,8 +508,10 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("class") }, ), - getInstanceFails: true, - isValid: false, + mockClientSettings: mockSettings{ + getInstanceFails: true, + }, + isValid: false, }, { description: "get storages fails", @@ -514,8 +522,10 @@ func TestBuildRequest(t *testing.T) { model.RAM = utils.Ptr(int64(4)) }, ), - listFlavorsFails: true, - isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, + isValid: false, }, { description: "invalid storage class", @@ -524,18 +534,20 @@ func TestBuildRequest(t *testing.T) { model.StorageClass = utils.Ptr("non-existing-class") }, ), - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -547,18 +559,20 @@ func TestBuildRequest(t *testing.T) { model.StorageSize = utils.Ptr(int64(9)) }, ), - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Flavor: &postgresflex.Flavor{ - Id: utils.Ptr(testFlavorId), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Flavor: &postgresflex.Flavor{ + Id: utils.Ptr(testFlavorId), + }, }, }, - }, - listStoragesResp: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"class"}, - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), + listStoragesResp: &postgresflex.ListStoragesResponse{ + StorageClasses: []string{"class"}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, }, }, isValid: false, @@ -567,15 +581,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - getInstanceFails: tt.getInstanceFails, - getInstanceResp: tt.getInstanceResp, - listFlavorsFails: tt.listFlavorsFails, - listFlavorsResp: tt.listFlavorsResp, - listStoragesFails: tt.listStoragesFails, - listStoragesResp: tt.listStoragesResp, - } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIClientMock(tt.mockClientSettings)) if err != nil { if !tt.isValid { return @@ -586,6 +592,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.IgnoreFields(tt.expectedRequest, "ApiService"), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -620,12 +627,10 @@ func Test_outputResult(t *testing.T) { }, }, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, true, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, true, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/options/options.go b/internal/cmd/postgresflex/options/options.go index c327e030b..c2e6e5b08 100644 --- a/internal/cmd/postgresflex/options/options.go +++ b/internal/cmd/postgresflex/options/options.go @@ -2,13 +2,13 @@ package options import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( @@ -38,9 +37,9 @@ type inputModel struct { } type options struct { - Flavors *[]postgresflex.Flavor `json:"flavors,omitempty"` - Versions *[]string `json:"versions,omitempty"` - Storages *flavorStorages `json:"flavorStorages,omitempty"` + Flavors []postgresflex.Flavor `json:"flavors,omitempty"` + Versions []string `json:"versions,omitempty"` + Storages *flavorStorages `json:"flavorStorages,omitempty"` } type flavorStorages struct { @@ -48,7 +47,7 @@ type flavorStorages struct { Storages *postgresflex.ListStoragesResponse `json:"storages"` } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", Short: "Lists PostgreSQL Flex options", @@ -65,9 +64,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List PostgreSQL Flex storage options for a given flavor. The flavor ID can be retrieved by running "$ stackit postgresflex options --flavors"`, "$ stackit postgresflex options --storages --flavor-id "), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -79,7 +78,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Call API - err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient) + err = buildAndExecuteRequest(ctx, params.Printer, model, apiClient.DefaultAPI) if err != nil { return fmt.Errorf("get PostgreSQL Flex options: %w", err) } @@ -98,7 +97,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(flavorIdFlag, "", `The flavor ID to show storages for. Only relevant when "--storages" is passed`) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -129,44 +128,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { FlavorId: flags.FlagToStringPointer(p, cmd, flavorIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type postgresFlexOptionsClient interface { - ListFlavorsExecute(ctx context.Context, projectId, region string) (*postgresflex.ListFlavorsResponse, error) - ListVersionsExecute(ctx context.Context, projectId, region string) (*postgresflex.ListVersionsResponse, error) - ListStoragesExecute(ctx context.Context, projectId, region, flavorId string) (*postgresflex.ListStoragesResponse, error) -} - -func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputModel, apiClient postgresFlexOptionsClient) error { +func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputModel, apiClient postgresflex.DefaultAPI) error { var flavors *postgresflex.ListFlavorsResponse var versions *postgresflex.ListVersionsResponse var storages *postgresflex.ListStoragesResponse var err error if model.Flavors { - flavors, err = apiClient.ListFlavorsExecute(ctx, model.ProjectId, model.Region) + flavors, err = apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() if err != nil { return fmt.Errorf("get PostgreSQL Flex flavors: %w", err) } } if model.Versions { - versions, err = apiClient.ListVersionsExecute(ctx, model.ProjectId, model.Region) + versions, err = apiClient.ListVersions(ctx, model.ProjectId, model.Region).Execute() if err != nil { return fmt.Errorf("get PostgreSQL Flex versions: %w", err) } } if model.Storages { - storages, err = apiClient.ListStoragesExecute(ctx, model.ProjectId, model.Region, *model.FlavorId) + storages, err = apiClient.ListStorages(ctx, model.ProjectId, model.Region, *model.FlavorId).Execute() if err != nil { return fmt.Errorf("get PostgreSQL Flex storages: %w", err) } @@ -193,45 +178,25 @@ func outputResult(p *print.Printer, model inputModel, flavors *postgresflex.List } } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(options, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex options: %w", err) + return p.OutputResult(model.OutputFormat, options, func() error { + content := []tables.Table{} + if model.Flavors && len(options.Flavors) != 0 { + content = append(content, buildFlavorsTable(options.Flavors)) } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if model.Versions && len(options.Versions) != 0 { + content = append(content, buildVersionsTable(options.Versions)) + } + if model.Storages && options.Storages.Storages != nil && len(options.Storages.Storages.StorageClasses) > 0 { + content = append(content, buildStoragesTable(*options.Storages.Storages)) + } + + err := tables.DisplayTables(p, content) if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex options: %w", err) + return fmt.Errorf("display output: %w", err) } - p.Outputln(string(details)) return nil - default: - return outputResultAsTable(p, model, options) - } -} - -func outputResultAsTable(p *print.Printer, model inputModel, options *options) error { - content := []tables.Table{} - if model.Flavors && len(*options.Flavors) != 0 { - content = append(content, buildFlavorsTable(*options.Flavors)) - } - if model.Versions && len(*options.Versions) != 0 { - content = append(content, buildVersionsTable(*options.Versions)) - } - if model.Storages && options.Storages.Storages != nil && len(*options.Storages.Storages.StorageClasses) > 0 { - content = append(content, buildStoragesTable(*options.Storages.Storages)) - } - - err := tables.DisplayTables(p, content) - if err != nil { - return fmt.Errorf("display output: %w", err) - } - - return nil + }) } func buildFlavorsTable(flavors []postgresflex.Flavor) tables.Table { @@ -262,12 +227,10 @@ func buildVersionsTable(versions []string) tables.Table { } func buildStoragesTable(storagesResp postgresflex.ListStoragesResponse) tables.Table { - storages := *storagesResp.StorageClasses table := tables.NewTable() table.SetTitle("Storages") table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") - for i := range storages { - sc := storages[i] + for _, sc := range storagesResp.StorageClasses { table.AddRow( utils.PtrString(storagesResp.StorageRange.Min), utils.PtrString(storagesResp.StorageRange.Max), diff --git a/internal/cmd/postgresflex/options/options_test.go b/internal/cmd/postgresflex/options/options_test.go index 54df1ccf9..f2798225f 100644 --- a/internal/cmd/postgresflex/options/options_test.go +++ b/internal/cmd/postgresflex/options/options_test.go @@ -5,13 +5,14 @@ import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} @@ -19,7 +20,7 @@ type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") var testProjectId = uuid.NewString() -type postgresFlexClientMocked struct { +type mockSettings struct { listFlavorsFails bool listVersionsFails bool listStoragesFails bool @@ -29,38 +30,40 @@ type postgresFlexClientMocked struct { listStoragesCalled bool } -func (c *postgresFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ string) (*postgresflex.ListFlavorsResponse, error) { - c.listFlavorsCalled = true - if c.listFlavorsFails { - return nil, fmt.Errorf("list flavors failed") - } - return utils.Ptr(postgresflex.ListFlavorsResponse{ - Flavors: utils.Ptr([]postgresflex.Flavor{}), - }), nil -} - -func (c *postgresFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*postgresflex.ListVersionsResponse, error) { - c.listVersionsCalled = true - if c.listVersionsFails { - return nil, fmt.Errorf("list versions failed") - } - return utils.Ptr(postgresflex.ListVersionsResponse{ - Versions: utils.Ptr([]string{}), - }), nil -} - -func (c *postgresFlexClientMocked) ListStoragesExecute(_ context.Context, _, _, _ string) (*postgresflex.ListStoragesResponse, error) { - c.listStoragesCalled = true - if c.listStoragesFails { - return nil, fmt.Errorf("list storages failed") +func newAPIClientMock(c *mockSettings) postgresflex.DefaultAPI { + return postgresflex.DefaultAPIServiceMock{ + ListFlavorsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListFlavorsRequest) (*postgresflex.ListFlavorsResponse, error) { + c.listFlavorsCalled = true + if c.listFlavorsFails { + return nil, fmt.Errorf("list flavors failed") + } + return utils.Ptr(postgresflex.ListFlavorsResponse{ + Flavors: []postgresflex.Flavor{}, + }), nil + }), + ListVersionsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListVersionsRequest) (*postgresflex.ListVersionsResponse, error) { + c.listVersionsCalled = true + if c.listVersionsFails { + return nil, fmt.Errorf("list versions failed") + } + return utils.Ptr(postgresflex.ListVersionsResponse{ + Versions: []string{}, + }), nil + }), + ListStoragesExecuteMock: utils.Ptr(func(_ postgresflex.ApiListStoragesRequest) (*postgresflex.ListStoragesResponse, error) { + c.listStoragesCalled = true + if c.listStoragesFails { + return nil, fmt.Errorf("list storages failed") + } + return utils.Ptr(postgresflex.ListStoragesResponse{ + StorageClasses: []string{}, + StorageRange: &postgresflex.StorageRange{ + Min: utils.Ptr(int64(10)), + Max: utils.Ptr(int64(100)), + }, + }), nil + }), } - return utils.Ptr(postgresflex.ListStoragesResponse{ - StorageClasses: utils.Ptr([]string{}), - StorageRange: &postgresflex.StorageRange{ - Min: utils.Ptr(int64(10)), - Max: utils.Ptr(int64(100)), - }, - }), nil } func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -113,6 +116,7 @@ func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -175,46 +179,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -224,9 +189,7 @@ func TestBuildAndExecuteRequest(t *testing.T) { description string model *inputModel isValid bool - listFlavorsFails bool - listVersionsFails bool - listStoragesFails bool + mockClientSettings mockSettings expectListFlavorsCalled bool expectListVersionsCalled bool expectListStoragesCalled bool @@ -269,28 +232,34 @@ func TestBuildAndExecuteRequest(t *testing.T) { expectListStoragesCalled: true, }, { - description: "list flavors fails", - model: fixtureInputModelAllTrue(), - isValid: false, - listFlavorsFails: true, + description: "list flavors fails", + model: fixtureInputModelAllTrue(), + isValid: false, + mockClientSettings: mockSettings{ + listFlavorsFails: true, + }, expectListFlavorsCalled: true, expectListVersionsCalled: false, expectListStoragesCalled: false, }, { - description: "list versions fails", - model: fixtureInputModelAllTrue(), - isValid: false, - listVersionsFails: true, + description: "list versions fails", + model: fixtureInputModelAllTrue(), + isValid: false, + mockClientSettings: mockSettings{ + listVersionsFails: true, + }, expectListFlavorsCalled: true, expectListVersionsCalled: true, expectListStoragesCalled: false, }, { - description: "list storages fails", - model: fixtureInputModelAllTrue(), - isValid: false, - listStoragesFails: true, + description: "list storages fails", + model: fixtureInputModelAllTrue(), + isValid: false, + mockClientSettings: mockSettings{ + listStoragesFails: true, + }, expectListFlavorsCalled: true, expectListVersionsCalled: true, expectListStoragesCalled: true, @@ -299,16 +268,9 @@ func TestBuildAndExecuteRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := &print.Printer{} - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - p.Cmd = cmd - client := &postgresFlexClientMocked{ - listFlavorsFails: tt.listFlavorsFails, - listVersionsFails: tt.listVersionsFails, - listStoragesFails: tt.listStoragesFails, - } - - err := buildAndExecuteRequest(testCtx, p, tt.model, client) + params := testparams.NewTestParams() + client := newAPIClientMock(&tt.mockClientSettings) + err := buildAndExecuteRequest(testCtx, params.Printer, tt.model, client) if err != nil && tt.isValid { t.Fatalf("error building and executing request: %v", err) } @@ -319,14 +281,14 @@ func TestBuildAndExecuteRequest(t *testing.T) { return } - if tt.expectListFlavorsCalled != client.listFlavorsCalled { - t.Fatalf("expected listFlavorsCalled to be %v, got %v", tt.expectListFlavorsCalled, client.listFlavorsCalled) + if tt.expectListFlavorsCalled != tt.mockClientSettings.listFlavorsCalled { + t.Fatalf("expected listFlavorsCalled to be %v, got %v", tt.expectListFlavorsCalled, tt.mockClientSettings.listFlavorsCalled) } - if tt.expectListVersionsCalled != client.listVersionsCalled { - t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, client.listVersionsCalled) + if tt.expectListVersionsCalled != tt.mockClientSettings.listVersionsCalled { + t.Fatalf("expected listVersionsCalled to be %v, got %v", tt.expectListVersionsCalled, tt.mockClientSettings.listVersionsCalled) } - if tt.expectListStoragesCalled != client.listStoragesCalled { - t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, client.listStoragesCalled) + if tt.expectListStoragesCalled != tt.mockClientSettings.listStoragesCalled { + t.Fatalf("expected listStoragesCalled to be %v, got %v", tt.expectListStoragesCalled, tt.mockClientSettings.listStoragesCalled) } }) } @@ -356,24 +318,23 @@ func Test_outputResult(t *testing.T) { args{ model: inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}, Flavors: false, Versions: false, Storages: false, FlavorId: new(string)}, flavors: &postgresflex.ListFlavorsResponse{ - Flavors: &[]postgresflex.Flavor{}, + Flavors: []postgresflex.Flavor{}, }, versions: &postgresflex.ListVersionsResponse{ - Versions: &[]string{}, + Versions: []string{}, }, storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{}, + StorageClasses: []string{}, StorageRange: &postgresflex.StorageRange{}, }, }, false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.flavors, tt.args.versions, tt.args.storages); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.flavors, tt.args.versions, tt.args.storages); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/postgresflex.go b/internal/cmd/postgresflex/postgresflex.go index 3a86086c2..536584f2f 100644 --- a/internal/cmd/postgresflex/postgresflex.go +++ b/internal/cmd/postgresflex/postgresflex.go @@ -1,18 +1,18 @@ package postgresflex import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/backup" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/options" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "postgresflex", Aliases: []string{"postgresqlflex"}, @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(user.NewCmd(params)) cmd.AddCommand(options.NewCmd(params)) diff --git a/internal/cmd/postgresflex/user/create/create.go b/internal/cmd/postgresflex/user/create/create.go index 00e214089..3ea952b06 100644 --- a/internal/cmd/postgresflex/user/create/create.go +++ b/internal/cmd/postgresflex/user/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,17 +18,21 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( instanceIdFlag = "instance-id" usernameFlag = "username" - roleFlag = "role" ) var ( rolesDefault = []string{"login"} + roleFlag = flags.StringEnumSliceFlag( + "role", + []string{"login", "createdb"}, + "Roles of the user,", + flags.DefaultValues(rolesDefault...), + ) ) type inputModel struct { @@ -35,10 +40,10 @@ type inputModel struct { InstanceId string Username *string - Roles *[]string + Roles []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a PostgreSQL Flex user", @@ -57,9 +62,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit postgresflex user create --instance-id xxx --username johndoe --role createdb"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,18 +75,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -100,17 +103,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - roleOptions := []string{"login", "createdb"} - cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") cmd.Flags().String(usernameFlag, "", "Username of the user") - cmd.Flags().Var(flags.EnumSliceFlag(false, rolesDefault, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q", roleOptions)) + roleFlag.Register(cmd) err := flags.MarkFlagsRequired(cmd, instanceIdFlag, usernameFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -120,23 +121,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), Username: flags.FlagToStringPointer(p, cmd, usernameFlag), - Roles: flags.FlagWithDefaultToStringSlicePointer(p, cmd, roleFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Roles: roleFlag.Get(), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiCreateUserRequest { - req := apiClient.CreateUser(ctx, model.ProjectId, model.Region, model.InstanceId) + req := apiClient.DefaultAPI.CreateUser(ctx, model.ProjectId, model.Region, model.InstanceId) req = req.CreateUserPayload(postgresflex.CreateUserPayload{ Username: model.Username, Roles: model.Roles, @@ -148,34 +141,18 @@ func outputResult(p *print.Printer, outputFormat, instanceLabel string, resp *po if resp == nil { return fmt.Errorf("no response passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgresFlex user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgresFlex user: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { if user := resp.Item; user != nil { p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, utils.PtrString(user.Id)) p.Outputf("Username: %s\n", utils.PtrString(user.Username)) p.Outputf("Password: %s\n", utils.PtrString(user.Password)) - p.Outputf("Roles: %v\n", utils.PtrString(user.Roles)) + p.Outputf("Roles: %v\n", user.Roles) p.Outputf("Host: %s\n", utils.PtrString(user.Host)) p.Outputf("Port: %s\n", utils.PtrString(user.Port)) p.Outputf("URI: %s\n", utils.PtrString(user.Uri)) } return nil - } + }) } diff --git a/internal/cmd/postgresflex/user/create/create_test.go b/internal/cmd/postgresflex/user/create/create_test.go index e75d785e9..82bf6cec8 100644 --- a/internal/cmd/postgresflex/user/create/create_test.go +++ b/internal/cmd/postgresflex/user/create/create_test.go @@ -7,18 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -29,7 +29,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st globalflags.RegionFlag: testRegion, instanceIdFlag: testInstanceId, usernameFlag: "johndoe", - roleFlag: "login", + roleFlag.Name(): "login", } for _, mod := range mods { mod(flagValues) @@ -46,7 +46,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { }, InstanceId: testInstanceId, Username: utils.Ptr("johndoe"), - Roles: utils.Ptr([]string{"login"}), + Roles: []string{"login"}, } for _, mod := range mods { mod(model) @@ -55,10 +55,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiCreateUserRequest)) postgresflex.ApiCreateUserRequest { - request := testClient.CreateUser(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.CreateUser(testCtx, testProjectId, testRegion, testInstanceId) request = request.CreateUserPayload(postgresflex.CreateUserPayload{ Username: utils.Ptr("johndoe"), - Roles: utils.Ptr([]string{"login"}), + Roles: []string{"login"}, }) for _, mod := range mods { @@ -70,6 +70,7 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiCreateUserRequest)) po func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -131,17 +132,17 @@ func TestParseInput(t *testing.T) { { description: "roles missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, roleFlag) + delete(flagValues, roleFlag.Name()) }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Roles = &rolesDefault + model.Roles = rolesDefault }), }, { description: "invalid role", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[roleFlag] = "invalid-role" + flagValues[roleFlag.Name()] = "invalid-role" }), isValid: false, }, @@ -149,48 +150,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -212,7 +172,7 @@ func TestBuildRequest(t *testing.T) { model.Username = nil }), expectedRequest: fixtureRequest().CreateUserPayload(postgresflex.CreateUserPayload{ - Roles: utils.Ptr([]string{"login"}), + Roles: []string{"login"}, }), }, } @@ -223,7 +183,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -252,18 +212,16 @@ func Test_outputResult(t *testing.T) { Id: new(string), Password: new(string), Port: new(int64), - Roles: &[]string{}, + Roles: []string{}, Uri: new(string), Username: new(string), }, }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/user/delete/delete.go b/internal/cmd/postgresflex/user/delete/delete.go index 925453632..f6667191c 100644 --- a/internal/cmd/postgresflex/user/delete/delete.go +++ b/internal/cmd/postgresflex/user/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", userIdArg), Short: "Deletes a PostgreSQL Flex user", @@ -59,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId, model.UserId) + userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -115,19 +114,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiDeleteUserRequest { - req := apiClient.DeleteUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.DeleteUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) return req } diff --git a/internal/cmd/postgresflex/user/delete/delete_test.go b/internal/cmd/postgresflex/user/delete/delete_test.go index 867635736..a91291466 100644 --- a/internal/cmd/postgresflex/user/delete/delete_test.go +++ b/internal/cmd/postgresflex/user/delete/delete_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiDeleteUserRequest)) postgresflex.ApiDeleteUserRequest { - request := testClient.DeleteUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) + request := testClient.DefaultAPI.DeleteUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) for _, mod := range mods { mod(&request) } @@ -160,54 +159,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -231,7 +183,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/postgresflex/user/describe/describe.go b/internal/cmd/postgresflex/user/describe/describe.go index fb18ff889..17a68f2e0 100644 --- a/internal/cmd/postgresflex/user/describe/describe.go +++ b/internal/cmd/postgresflex/user/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", userIdArg), Short: "Shows details of a PostgreSQL Flex user", @@ -68,7 +68,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() if err != nil { - return fmt.Errorf("get MongoDB Flex user: %w", err) + return fmt.Errorf("get postgresflex user: %w", err) } return outputResult(params.Printer, model.OutputFormat, *resp.Item) @@ -100,48 +100,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiGetUserRequest { - req := apiClient.GetUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.GetUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) return req } func outputResult(p *print.Printer, outputFormat string, user postgresflex.UserResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, user, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(user.Id)) table.AddSeparator() table.AddRow("USERNAME", utils.PtrString(user.Username)) table.AddSeparator() - table.AddRow("ROLES", utils.PtrString(user.Roles)) + table.AddRow("ROLES", user.Roles) table.AddSeparator() table.AddRow("HOST", utils.PtrString(user.Host)) table.AddSeparator() @@ -153,5 +128,5 @@ func outputResult(p *print.Printer, outputFormat string, user postgresflex.UserR } return nil - } + }) } diff --git a/internal/cmd/postgresflex/user/describe/describe_test.go b/internal/cmd/postgresflex/user/describe/describe_test.go index 8b91301b7..e5f64a31f 100644 --- a/internal/cmd/postgresflex/user/describe/describe_test.go +++ b/internal/cmd/postgresflex/user/describe/describe_test.go @@ -7,16 +7,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" @@ -61,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiGetUserRequest)) postgresflex.ApiGetUserRequest { - request := testClient.GetUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) + request := testClient.DefaultAPI.GetUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) for _, mod := range mods { mod(&request) } @@ -159,54 +160,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -230,7 +184,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -255,16 +209,14 @@ func Test_outputResult(t *testing.T) { Host: new(string), Id: new(string), Port: new(int64), - Roles: &[]string{}, + Roles: []string{}, Username: new(string), }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/user/list/list.go b/internal/cmd/postgresflex/user/list/list.go index a493855e2..abe95d1a4 100644 --- a/internal/cmd/postgresflex/user/list/list.go +++ b/internal/cmd/postgresflex/user/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( @@ -29,11 +29,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - InstanceId *string + InstanceId string Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all PostgreSQL Flex users of an instance", @@ -50,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit postgresflex user list --instance-id xxx --limit 10"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,23 +69,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get PostgreSQL Flex users: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, *model.InstanceId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = *model.InstanceId - } - params.Printer.Info("No users found for instance %q\n", instanceLabel) - return nil - } - users := *resp.Items + users := resp.Items // Truncate output if model.Limit != nil && len(users) > int(*model.Limit) { users = users[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, users) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId + } + return outputResult(params.Printer, model.OutputFormat, instanceLabel, users) }, } @@ -101,7 +97,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -117,46 +113,25 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiListUsersRequest { - req := apiClient.ListUsers(ctx, model.ProjectId, model.Region, *model.InstanceId) + req := apiClient.DefaultAPI.ListUsers(ctx, model.ProjectId, model.Region, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, users []postgresflex.ListUsersResponseItem) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(users, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex user list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex user list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, users []postgresflex.ListUsersResponseItem) error { + return p.OutputResult(outputFormat, users, func() error { + if len(users) == 0 { + p.Outputf("No users found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "USERNAME") for i := range users { @@ -172,5 +147,5 @@ func outputResult(p *print.Printer, outputFormat string, users []postgresflex.Li } return nil - } + }) } diff --git a/internal/cmd/postgresflex/user/list/list_test.go b/internal/cmd/postgresflex/user/list/list_test.go index 20adc9e13..808079f8a 100644 --- a/internal/cmd/postgresflex/user/list/list_test.go +++ b/internal/cmd/postgresflex/user/list/list_test.go @@ -7,18 +7,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testRegion = "eu01" @@ -43,7 +43,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceId: utils.Ptr(testInstanceId), + InstanceId: testInstanceId, Limit: utils.Ptr(int64(10)), } for _, mod := range mods { @@ -53,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiListUsersRequest)) postgresflex.ApiListUsersRequest { - request := testClient.ListUsers(testCtx, testProjectId, testRegion, testInstanceId) + request := testClient.DefaultAPI.ListUsers(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -63,6 +63,7 @@ func fixtureRequest(mods ...func(request *postgresflex.ApiListUsersRequest)) pos func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -131,48 +132,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -196,7 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -207,8 +167,9 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { - outputFormat string - users []postgresflex.ListUsersResponseItem + outputFormat string + instanceLabel string + users []postgresflex.ListUsersResponseItem } tests := []struct { name string @@ -217,17 +178,15 @@ func Test_outputResult(t *testing.T) { }{ {"empty", args{}, false}, {"standard", args{users: []postgresflex.ListUsersResponseItem{{}}}, false}, - {"complete", args{users: []postgresflex.ListUsersResponseItem{{ + {"complete", args{instanceLabel: "label", users: []postgresflex.ListUsersResponseItem{{ Id: new(string), Username: new(string), }}}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.users); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.users); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/user/reset-password/reset_password.go b/internal/cmd/postgresflex/user/reset-password/reset_password.go index 45255b988..7a6ce4efb 100644 --- a/internal/cmd/postgresflex/user/reset-password/reset_password.go +++ b/internal/cmd/postgresflex/user/reset-password/reset_password.go @@ -2,12 +2,13 @@ package resetpassword import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/client" postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("reset-password %s", userIdArg), Short: "Resets the password of a PostgreSQL Flex user", @@ -60,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId, model.UserId) + userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to reset the password of user %q of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -116,45 +114,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiResetUserRequest { - req := apiClient.ResetUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.ResetUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) return req } func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel string, user *postgresflex.ResetUserResponse) error { - if user == nil { - return fmt.Errorf("no response passed") - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgresFlex user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgresFlex user: %w", err) + return p.OutputResult(outputFormat, user, func() error { + if user == nil { + return fmt.Errorf("no response passed") } - p.Outputln(string(details)) - - return nil - default: p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel) if item := user.Item; item != nil { p.Outputf("Username: %s\n", utils.PtrString(item.Username)) @@ -162,5 +135,5 @@ func outputResult(p *print.Printer, outputFormat, userLabel, instanceLabel strin p.Outputf("New URI: %s\n", utils.PtrString(item.Uri)) } return nil - } + }) } diff --git a/internal/cmd/postgresflex/user/reset-password/reset_password_test.go b/internal/cmd/postgresflex/user/reset-password/reset_password_test.go index b12c31d3a..4ee3fc726 100644 --- a/internal/cmd/postgresflex/user/reset-password/reset_password_test.go +++ b/internal/cmd/postgresflex/user/reset-password/reset_password_test.go @@ -7,16 +7,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" @@ -61,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiResetUserRequest)) postgresflex.ApiResetUserRequest { - request := testClient.ResetUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) + request := testClient.DefaultAPI.ResetUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) for _, mod := range mods { mod(&request) } @@ -159,54 +160,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -230,7 +184,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -260,12 +214,10 @@ func Test_outputResult(t *testing.T) { Item: &postgresflex.User{}, }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) - + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.userLabel, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.userLabel, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/postgresflex/user/update/update.go b/internal/cmd/postgresflex/user/update/update.go index 4149a3b78..e76bce64c 100644 --- a/internal/cmd/postgresflex/user/update/update.go +++ b/internal/cmd/postgresflex/user/update/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,14 +16,19 @@ import ( postgresflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/postgresflex/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) const ( userIdArg = "USER_ID" instanceIdFlag = "instance-id" - roleFlag = "role" +) + +var roleFlag = flags.StringEnumSliceFlag( + "role", + []string{"login", "createdb"}, + "Roles of the user,", ) type inputModel struct { @@ -30,10 +36,10 @@ type inputModel struct { InstanceId string UserId string - Roles *[]string + Roles []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", userIdArg), Short: "Updates a PostgreSQL Flex user", @@ -57,24 +63,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId) + instanceLabel, err := postgresflexUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient, model.ProjectId, model.Region, model.InstanceId, model.UserId) + userLabel, err := postgresflexUtils.GetUserName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user name: %v", err) userLabel = model.UserId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update user %q of instance %q?", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -94,10 +98,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - roleOptions := []string{"login", "createdb"} - cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "ID of the instance") - cmd.Flags().Var(flags.EnumSliceFlag(false, nil, roleOptions...), roleFlag, fmt.Sprintf("Roles of the user, possible values are %q", roleOptions)) + roleFlag.Register(cmd) err := flags.MarkFlagsRequired(cmd, instanceIdFlag) cobra.CheckErr(err) @@ -111,8 +113,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return nil, &errors.ProjectIdError{} } - roles := flags.FlagToStringSlicePointer(p, cmd, roleFlag) - if roles == nil { + roles := roleFlag.Get() + if len(roles) == 0 { return nil, &errors.EmptyUpdateError{} } @@ -123,20 +125,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Roles: roles, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *postgresflex.APIClient) postgresflex.ApiPartialUpdateUserRequest { - req := apiClient.PartialUpdateUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.PartialUpdateUser(ctx, model.ProjectId, model.Region, model.InstanceId, model.UserId) req = req.PartialUpdateUserPayload(postgresflex.PartialUpdateUserPayload{ Roles: model.Roles, }) diff --git a/internal/cmd/postgresflex/user/update/update_test.go b/internal/cmd/postgresflex/user/update/update_test.go index 787dc992d..89a194b4b 100644 --- a/internal/cmd/postgresflex/user/update/update_test.go +++ b/internal/cmd/postgresflex/user/update/update_test.go @@ -4,21 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &postgresflex.APIClient{} +var testClient = &postgresflex.APIClient{DefaultAPI: &postgresflex.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = "12345" @@ -39,7 +37,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, instanceIdFlag: testInstanceId, - roleFlag: "login", + roleFlag.Name(): "login", } for _, mod := range mods { mod(flagValues) @@ -56,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { }, InstanceId: testInstanceId, UserId: testUserId, - Roles: utils.Ptr([]string{"login"}), + Roles: []string{"login"}, } for _, mod := range mods { mod(model) @@ -65,9 +63,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *postgresflex.ApiPartialUpdateUserRequest)) postgresflex.ApiPartialUpdateUserRequest { - request := testClient.PartialUpdateUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) + request := testClient.DefaultAPI.PartialUpdateUser(testCtx, testProjectId, testRegion, testInstanceId, testUserId) request = request.PartialUpdateUserPayload(postgresflex.PartialUpdateUserPayload{ - Roles: utils.Ptr([]string{"login"}), + Roles: []string{"login"}, }) for _, mod := range mods { mod(&request) @@ -153,7 +151,7 @@ func TestParseInput(t *testing.T) { description: "invalid role", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[roleFlag] = "invalid-role" + flagValues[roleFlag.Name()] = "invalid-role" }), isValid: false, }, @@ -161,7 +159,7 @@ func TestParseInput(t *testing.T) { description: "empty update", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, roleFlag) + delete(flagValues, roleFlag.Name()) }), isValid: false, }, @@ -169,54 +167,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -240,7 +191,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, postgresflex.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/postgresflex/user/user.go b/internal/cmd/postgresflex/user/user.go index af9d371ef..ce566f5b7 100644 --- a/internal/cmd/postgresflex/user/user.go +++ b/internal/cmd/postgresflex/user/user.go @@ -1,7 +1,6 @@ package user import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user/create" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user/describe" @@ -9,12 +8,13 @@ import ( resetpassword "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user/reset-password" "github.com/stackitcloud/stackit-cli/internal/cmd/postgresflex/user/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "user", Short: "Provides functionality for PostgreSQL Flex users", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/project/create/create.go b/internal/cmd/project/create/create.go index 17e9e950e..ef95167e9 100644 --- a/internal/cmd/project/create/create.go +++ b/internal/cmd/project/create/create.go @@ -2,12 +2,11 @@ package create import ( "context" - "encoding/json" "fmt" "regexp" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -16,10 +15,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) const ( @@ -36,13 +34,13 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ParentId *string - Name *string + ParentId string + Name string Labels *map[string]string NetworkAreaId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a STACKIT project", @@ -65,9 +63,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a STACKIT project with a network area`, "$ stackit project create --parent-id xxxx --name my-project --network-area-id yyyy"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -78,12 +76,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a project under the parent with ID %q?", *model.ParentId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a project under the parent with ID %q?", model.ParentId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -113,7 +109,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) labels := flags.FlagToStringToStringPointer(p, cmd, labelFlag) @@ -139,26 +135,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - ParentId: flags.FlagToStringPointer(p, cmd, parentIdFlag), - Name: flags.FlagToStringPointer(p, cmd, nameFlag), + ParentId: flags.FlagToStringValue(p, cmd, parentIdFlag), + Name: flags.FlagToStringValue(p, cmd, nameFlag), Labels: labels, NetworkAreaId: flags.FlagToStringPointer(p, cmd, networkAreaIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) (resourcemanager.ApiCreateProjectRequest, error) { - req := apiClient.CreateProject(ctx) + req := apiClient.DefaultAPI.CreateProject(ctx) authFlow, err := auth.GetAuthFlow() if err != nil { @@ -202,10 +190,10 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *resourceman ContainerParentId: model.ParentId, Name: model.Name, Labels: labels, - Members: &[]resourcemanager.Member{ + Members: []resourcemanager.Member{ { - Role: utils.Ptr(ownerRole), - Subject: utils.Ptr(email), + Role: ownerRole, + Subject: email, }, }, }) @@ -220,25 +208,8 @@ func outputResult(p *print.Printer, model inputModel, resp *resourcemanager.Proj if model.GlobalFlagModel == nil { return fmt.Errorf("globalflags are empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal project: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal project: %w", err) - } - p.Outputln(string(details)) - + return p.OutputResult(model.OutputFormat, resp, func() error { + p.Outputf("Created project under the parent with ID %q. Project ID: %s\n", model.ParentId, resp.ProjectId) return nil - default: - p.Outputf("Created project under the parent with ID %q. Project ID: %s\n", utils.PtrString(model.ParentId), utils.PtrString(resp.ProjectId)) - return nil - } + }) } diff --git a/internal/cmd/project/create/create_test.go b/internal/cmd/project/create/create_test.go index f6cf9fbde..e063c6990 100644 --- a/internal/cmd/project/create/create_test.go +++ b/internal/cmd/project/create/create_test.go @@ -7,19 +7,20 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + "github.com/zalando/go-keyring" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/zalando/go-keyring" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &resourcemanager.APIClient{} +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} var testParentId = uuid.NewString() var testNetworkAreaId = uuid.NewString() var testEmail = "email" @@ -40,8 +41,8 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{Verbosity: globalflags.VerbosityDefault}, - ParentId: utils.Ptr(testParentId), - Name: utils.Ptr(nameFlag), + ParentId: testParentId, + Name: nameFlag, Labels: utils.Ptr(map[string]string{ "key": "value", }), @@ -55,16 +56,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func fixturePayload(mods ...func(payload *resourcemanager.CreateProjectPayload)) resourcemanager.CreateProjectPayload { payload := resourcemanager.CreateProjectPayload{ - ContainerParentId: utils.Ptr(testParentId), - Name: utils.Ptr(nameFlag), + ContainerParentId: testParentId, + Name: nameFlag, Labels: utils.Ptr(map[string]string{ "key": "value", networkAreaLabel: testNetworkAreaId, }), - Members: &[]resourcemanager.Member{ + Members: []resourcemanager.Member{ { - Role: utils.Ptr(ownerRole), - Subject: utils.Ptr(testEmail), + Role: ownerRole, + Subject: testEmail, }, }, } @@ -75,7 +76,7 @@ func fixturePayload(mods ...func(payload *resourcemanager.CreateProjectPayload)) } func fixtureRequest(mods ...func(request *resourcemanager.ApiCreateProjectRequest)) resourcemanager.ApiCreateProjectRequest { - request := testClient.CreateProject(testCtx) + request := testClient.DefaultAPI.CreateProject(testCtx) request = request.CreateProjectPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -86,6 +87,7 @@ func fixtureRequest(mods ...func(request *resourcemanager.ApiCreateProjectReques func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string labelValues []string isValid bool @@ -177,56 +179,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.labelValues { - err := cmd.Flags().Set(labelFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", labelFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + labelFlag: tt.labelValues, + }, tt.isValid) }) } } @@ -351,7 +306,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -374,11 +329,10 @@ func Test_outputResult(t *testing.T) { {"base", args{inputModel{GlobalFlagModel: &globalflags.GlobalFlagModel{}}, &resourcemanager.Project{}}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/project/delete/delete.go b/internal/cmd/project/delete/delete.go index 4e83d9324..8975bad8d 100644 --- a/internal/cmd/project/delete/delete.go +++ b/internal/cmd/project/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,14 +15,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "delete", Short: "Deletes a STACKIT project", @@ -35,9 +36,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Delete a STACKIT project by explicitly providing the project ID`, "$ stackit project delete --project-id xxx"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -54,12 +55,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -80,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -90,19 +89,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiDeleteProjectRequest { - req := apiClient.DeleteProject(ctx, model.ProjectId) + req := apiClient.DefaultAPI.DeleteProject(ctx, model.ProjectId) return req } diff --git a/internal/cmd/project/delete/delete_test.go b/internal/cmd/project/delete/delete_test.go index b53ede53d..32c09895a 100644 --- a/internal/cmd/project/delete/delete_test.go +++ b/internal/cmd/project/delete/delete_test.go @@ -5,20 +5,19 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) type testCtxKey struct{} var projectIdFlag = globalflags.ProjectIdFlag var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &resourcemanager.APIClient{} +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -45,7 +44,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *resourcemanager.ApiDeleteProjectRequest)) resourcemanager.ApiDeleteProjectRequest { - request := testClient.DeleteProject(testCtx, testProjectId) + request := testClient.DefaultAPI.DeleteProject(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -55,6 +54,7 @@ func fixtureRequest(mods ...func(request *resourcemanager.ApiDeleteProjectReques func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -74,46 +74,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -137,7 +98,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/project/describe/describe.go b/internal/cmd/project/describe/describe.go index edd17d483..7498cd78c 100644 --- a/internal/cmd/project/describe/describe.go +++ b/internal/cmd/project/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { IncludeParents bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "describe", Short: "Shows details of a STACKIT project", @@ -88,7 +87,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" && projectId == "" { - return nil, fmt.Errorf("Project ID needs to be provided either as an argument or as a flag") + return nil, fmt.Errorf("project ID needs to be provided either as an argument or as a flag") } if projectId == "" { @@ -101,20 +100,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu IncludeParents: flags.FlagToBoolValue(p, cmd, includeParentsFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiGetProjectRequest { - req := apiClient.GetProject(ctx, model.ArgProjectId) + req := apiClient.DefaultAPI.GetProject(ctx, model.ArgProjectId) req.IncludeParents(model.IncludeParents) return req } @@ -123,41 +114,23 @@ func outputResult(p *print.Printer, outputFormat string, project *resourcemanage if project == nil { return fmt.Errorf("response not set") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(project, "", " ") - if err != nil { - return fmt.Errorf("marshal project details: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(project, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal project details: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, project, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(project.ProjectId)) + table.AddRow("ID", project.ProjectId) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(project.Name)) + table.AddRow("NAME", project.Name) table.AddSeparator() - table.AddRow("CREATION", utils.PtrString(project.CreationTime)) + table.AddRow("CREATION", project.CreationTime) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(project.LifecycleState)) + table.AddRow("STATE", project.LifecycleState) table.AddSeparator() - if project.Parent != nil { - table.AddRow("PARENT ID", utils.PtrString(project.Parent.Id)) - } + table.AddRow("PARENT ID", project.Parent.Id) err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/project/describe/describe_test.go b/internal/cmd/project/describe/describe_test.go index f32dbc491..92f07d0d7 100644 --- a/internal/cmd/project/describe/describe_test.go +++ b/internal/cmd/project/describe/describe_test.go @@ -8,11 +8,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &resourcemanager.APIClient{} +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} var testProjectId = uuid.NewString() var testProjectId2 = uuid.NewString() @@ -60,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *resourcemanager.ApiGetProjectRequest)) resourcemanager.ApiGetProjectRequest { - request := testClient.GetProject(testCtx, testProjectId) + request := testClient.DefaultAPI.GetProject(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -137,54 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -208,7 +161,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -230,19 +183,18 @@ func Test_outputResult(t *testing.T) { {"empty", args{}, true}, {"base", args{"", &resourcemanager.GetProjectResponse{}}, false}, {"complete", args{"", &resourcemanager.GetProjectResponse{ - ProjectId: utils.Ptr("4711"), - Name: utils.Ptr("name"), - CreationTime: utils.Ptr(time.Now()), - LifecycleState: utils.Ptr(resourcemanager.LIFECYCLESTATE_CREATING), - Parent: &resourcemanager.Parent{Id: utils.Ptr("parent id")}, + ProjectId: "4711", + Name: "name", + CreationTime: time.Now(), + LifecycleState: resourcemanager.LIFECYCLESTATE_CREATING, + Parent: resourcemanager.Parent{Id: "parent id"}, }, }, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.project); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.project); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/project/list/list.go b/internal/cmd/project/list/list.go index 364458a7c..82f4f19ee 100644 --- a/internal/cmd/project/list/list.go +++ b/internal/cmd/project/list/list.go @@ -2,13 +2,14 @@ package list import ( "context" - "encoding/json" "fmt" "time" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -18,8 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" ) const ( @@ -44,7 +43,7 @@ type inputModel struct { PageSize int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists STACKIT projects", @@ -64,9 +63,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all STACKIT projects that a certain user is a member of`, "$ stackit project list --member example@email.com"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -78,14 +77,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Fetch projects - projects, err := fetchProjects(ctx, model, apiClient) + projects, err := fetchProjects(ctx, model, apiClient.DefaultAPI) if err != nil { return err } - if len(projects) == 0 { - params.Printer.Info("No projects found matching the criteria\n") - return nil - } return outputResult(params.Printer, model.OutputFormat, projects) }, @@ -103,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(pageSizeFlag, pageSizeDefault, "Number of items fetched in each API call. Does not affect the number of items in the command output") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) creationTimeAfter, err := flags.FlagToDateTimePointer(p, cmd, creationTimeAfterFlag, creationTimeAfterFormat) @@ -140,15 +135,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { PageSize: pageSize, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -200,7 +187,7 @@ func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceMan if err != nil { return nil, fmt.Errorf("get projects: %w", err) } - respProjects := *resp.Items + respProjects := resp.GetItems() if len(respProjects) == 0 { break } @@ -221,38 +208,21 @@ func fetchProjects(ctx context.Context, model *inputModel, apiClient resourceMan } func outputResult(p *print.Printer, outputFormat string, projects []resourcemanager.Project) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(projects, "", " ") - if err != nil { - return fmt.Errorf("marshal projects list: %w", err) + return p.OutputResult(outputFormat, projects, func() error { + if len(projects) == 0 { + p.Outputf("No projects found matching the criteria\n") + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(projects, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal projects list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATE", "PARENT ID") for i := range projects { p := projects[i] - var parentId *string - if p.Parent != nil { - parentId = p.Parent.Id - } table.AddRow( - utils.PtrString(p.ProjectId), - utils.PtrString(p.Name), - utils.PtrString(p.LifecycleState), - utils.PtrString(parentId), + p.ProjectId, + p.Name, + p.LifecycleState, + p.Parent.Id, ) } @@ -262,5 +232,5 @@ func outputResult(p *print.Printer, outputFormat string, projects []resourcemana } return nil - } + }) } diff --git a/internal/cmd/project/list/list_test.go b/internal/cmd/project/list/list_test.go index 6d187a9bc..a656b2e5d 100644 --- a/internal/cmd/project/list/list_test.go +++ b/internal/cmd/project/list/list_test.go @@ -9,23 +9,25 @@ import ( "testing" "time" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + "github.com/zalando/go-keyring" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - "github.com/zalando/go-keyring" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &resourcemanager.APIClient{} +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} var testParentId = uuid.NewString() var testProjectIdLike = uuid.NewString() var testCreationTimeAfter = "2023-01-01T00:00:00Z" @@ -62,12 +64,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *resourcemanager.ApiListProjectsRequest)) resourcemanager.ApiListProjectsRequest { - request := testClient.ListProjects(testCtx) + request := testClient.DefaultAPI.ListProjects(testCtx) request = request.ContainerParentId(testParentId) testCreationTimeAfter, err := time.Parse(creationTimeAfterFormat, testCreationTimeAfter) if err != nil { - return resourcemanager.ListProjectsRequest{} + return resourcemanager.ApiListProjectsRequest{} } request = request.CreationTimeStart(testCreationTimeAfter) request = request.Member("member") @@ -81,6 +83,7 @@ func fixtureRequest(mods ...func(request *resourcemanager.ApiListProjectsRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string projectIdLikevalues *[]string isValid bool @@ -194,66 +197,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if tt.projectIdLikevalues != nil { - for _, value := range *tt.projectIdLikevalues { - err := cmd.Flags().Set(projectIdLikeFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", projectIdLikeFlag, value, err) - } - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating one of required flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + projectIdLikeFlag: utils.GetSliceFromPointer(tt.projectIdLikevalues), + }, tt.isValid) }) } } @@ -300,7 +246,7 @@ func TestBuildRequest(t *testing.T) { PageSize: pageSizeDefault, }, offset: 1, - expectedRequest: testClient.ListProjects(testCtx).Offset(1).Limit(pageSizeDefault).Member(authUserEmail), + expectedRequest: testClient.DefaultAPI.ListProjects(testCtx).Offset(1).Limit(pageSizeDefault).Member(authUserEmail), }, { description: "projectIdLike set", @@ -316,14 +262,14 @@ func TestBuildRequest(t *testing.T) { if tt.projectIdLike != nil { tt.model.ProjectIdLike = tt.projectIdLike } - request, err := buildRequest(testCtx, tt.model, testClient, tt.offset) + request, err := buildRequest(testCtx, tt.model, testClient.DefaultAPI, tt.offset) if err != nil { t.Fatalf("Failed to build request: %v", err) } diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -458,7 +404,7 @@ func TestFetchProjects(t *testing.T) { projects := make([]resourcemanager.Project, numItemsToReturn) mockedResp := resourcemanager.ListProjectsResponse{ - Items: &projects, + Items: projects, } mockedRespBytes, err := json.Marshal(mockedResp) @@ -481,7 +427,7 @@ func TestFetchProjects(t *testing.T) { t.Fatalf("Failed to initialize client: %v", err) } - projects, err := fetchProjects(testCtx, tt.model, client) + projects, err := fetchProjects(testCtx, tt.model, client.DefaultAPI) if err != nil { if !tt.apiCallFails { t.Fatalf("did not fail on invalid input") @@ -515,36 +461,35 @@ func Test_outputResult(t *testing.T) { {"base", args{"", []resourcemanager.Project{{}}}, false}, {"complete", args{"", []resourcemanager.Project{ { - ContainerId: utils.Ptr("container-id1"), - CreationTime: utils.Ptr(time.Now()), + ContainerId: "container-id1", + CreationTime: time.Now(), Labels: &map[string]string{"foo": "bar"}, - LifecycleState: utils.Ptr(resourcemanager.LIFECYCLESTATE_CREATING), - Name: utils.Ptr("some name"), - Parent: &resourcemanager.Parent{ - Id: utils.Ptr("parent-id"), + LifecycleState: resourcemanager.LIFECYCLESTATE_CREATING, + Name: "some name", + Parent: resourcemanager.Parent{ + Id: "parent-id", }, - ProjectId: utils.Ptr("project-id1"), + ProjectId: "project-id1", }, { - ContainerId: utils.Ptr("container-id2"), - CreationTime: utils.Ptr(time.Now()), + ContainerId: "container-id2", + CreationTime: time.Now(), Labels: &map[string]string{"foo": "bar"}, - LifecycleState: utils.Ptr(resourcemanager.LIFECYCLESTATE_CREATING), - Name: utils.Ptr("some name"), - Parent: &resourcemanager.Parent{ - Id: utils.Ptr("parent-id"), + LifecycleState: resourcemanager.LIFECYCLESTATE_CREATING, + Name: "some name", + Parent: resourcemanager.Parent{ + Id: "parent-id", }, - ProjectId: utils.Ptr("project-id2"), + ProjectId: "project-id2", }, }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projects); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projects); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/project/member/add/add.go b/internal/cmd/project/member/add/add.go index 1d40c0ec5..0901fba52 100644 --- a/internal/cmd/project/member/add/add.go +++ b/internal/cmd/project/member/add/add.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -34,7 +35,7 @@ type inputModel struct { Role *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("add %s", subjectArg), Short: "Adds a member to a project", @@ -70,12 +71,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to add the role %q to %s on project %q?", *model.Role, model.Subject, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to add the role %q to %s on project %q?", *model.Role, model.Subject, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -114,20 +113,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Role: flags.FlagToStringPointer(p, cmd, roleFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *authorization.APIClient) authorization.ApiAddMembersRequest { - req := apiClient.AddMembers(ctx, model.GlobalFlagModel.ProjectId) + req := apiClient.AddMembers(ctx, model.ProjectId) req = req.AddMembersPayload(authorization.AddMembersPayload{ Members: utils.Ptr([]authorization.Member{ { diff --git a/internal/cmd/project/member/add/add_test.go b/internal/cmd/project/member/add/add_test.go index 9d24461bf..fa8cb5f04 100644 --- a/internal/cmd/project/member/add/add_test.go +++ b/internal/cmd/project/member/add/add_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -124,54 +123,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/project/member/list/list.go b/internal/cmd/project/member/list/list.go index 673b4f755..1dd53f369 100644 --- a/internal/cmd/project/member/list/list.go +++ b/internal/cmd/project/member/list/list.go @@ -2,13 +2,14 @@ package list import ( "context" - "encoding/json" "fmt" "sort" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,17 +20,22 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/authorization/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) const ( subjectFlag = "subject" limitFlag = "limit" - sortByFlag = "sort-by" projectResourceType = "project" ) +var sortByFlag = flags.StringEnumFlag( + "sort-by", + []string{"subject", "role"}, + "Sort entries by a specific field,", + flags.StringEnumDefaultValue("subject"), +) + type inputModel struct { *globalflags.GlobalFlagModel @@ -38,7 +44,7 @@ type inputModel struct { SortBy string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists members of a project", @@ -55,9 +61,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 members of a project`, "$ stackit project member list --project-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -98,14 +104,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - sortByFlagOptions := []string{"subject", "role"} - cmd.Flags().String(subjectFlag, "", "Filter by subject (the identifier of a user, service account or client). This is usually the email address (for users) or name (for clients)") cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") - cmd.Flags().Var(flags.EnumFlag(false, "subject", sortByFlagOptions...), sortByFlag, fmt.Sprintf("Sort entries by a specific field, one of %q", sortByFlagOptions)) + sortByFlag.Register(cmd.Flags()) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,23 +127,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, Subject: flags.FlagToStringPointer(p, cmd, subjectFlag), Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), - SortBy: flags.FlagWithDefaultToStringValue(p, cmd, sortByFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + SortBy: sortByFlag.Get(), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *authorization.APIClient) authorization.ApiListMembersRequest { - req := apiClient.ListMembers(ctx, projectResourceType, model.GlobalFlagModel.ProjectId) + req := apiClient.ListMembers(ctx, projectResourceType, model.ProjectId) if model.Subject != nil { req = req.Subject(*model.Subject) } @@ -162,25 +158,7 @@ func outputResult(p *print.Printer, model inputModel, members []authorization.Me } sort.SliceStable(members, sortFn) - switch model.OutputFormat { - case print.JSONOutputFormat: - // Show details - details, err := json.MarshalIndent(members, "", " ") - if err != nil { - return fmt.Errorf("marshal members: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(members, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal members: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, members, func() error { table := tables.NewTable() table.SetHeader("SUBJECT", "ROLE") for i := range members { @@ -192,9 +170,10 @@ func outputResult(p *print.Printer, model inputModel, members []authorization.Me table.AddRow(utils.PtrString(m.Subject), utils.PtrString(m.Role)) } - if model.SortBy == "subject" { + switch model.SortBy { + case "subject": table.EnableAutoMergeOnColumns(1) - } else if model.SortBy == "role" { + case "role": table.EnableAutoMergeOnColumns(2) } @@ -204,5 +183,5 @@ func outputResult(p *print.Printer, model inputModel, members []authorization.Me } return nil - } + }) } diff --git a/internal/cmd/project/member/list/list_test.go b/internal/cmd/project/member/list/list_test.go index a710f7b5d..4c31dc91c 100644 --- a/internal/cmd/project/member/list/list_test.go +++ b/internal/cmd/project/member/list/list_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -60,6 +60,7 @@ func fixtureRequest(mods ...func(request *authorization.ApiListMembersRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -111,7 +112,7 @@ func TestParseInput(t *testing.T) { { description: "sort by role", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[sortByFlag] = "role" + flagValues[sortByFlag.Name()] = "role" }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { @@ -121,7 +122,7 @@ func TestParseInput(t *testing.T) { { description: "sort by invalid", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[sortByFlag] = "invalid" + flagValues[sortByFlag.Name()] = "invalid" }), isValid: false, }, @@ -129,48 +130,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -240,11 +200,10 @@ func Test_outputResult(t *testing.T) { }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.members); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.members); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/project/member/member.go b/internal/cmd/project/member/member.go index cef271820..4b247e877 100644 --- a/internal/cmd/project/member/member.go +++ b/internal/cmd/project/member/member.go @@ -1,17 +1,17 @@ package member import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/project/member/add" "github.com/stackitcloud/stackit-cli/internal/cmd/project/member/list" "github.com/stackitcloud/stackit-cli/internal/cmd/project/member/remove" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "member", Short: "Manages project members", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(add.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(remove.NewCmd(params)) diff --git a/internal/cmd/project/member/remove/remove.go b/internal/cmd/project/member/remove/remove.go index b4030199f..70a7c46d6 100644 --- a/internal/cmd/project/member/remove/remove.go +++ b/internal/cmd/project/member/remove/remove.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -36,7 +37,7 @@ type inputModel struct { Force bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("remove %s", subjectArg), Short: "Removes a member from a project", @@ -73,15 +74,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to remove the role %q from %s on project %q?", *model.Role, model.Subject, projectLabel) - if model.Force { - prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) - } - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to remove the role %q from %s on project %q?", *model.Role, model.Subject, projectLabel) + if model.Force { + prompt = fmt.Sprintf("%s This will also remove other roles of the subject that would stop the removal of the requested role", prompt) + } + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -122,20 +121,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Force: flags.FlagToBoolValue(p, cmd, forceFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *authorization.APIClient) authorization.ApiRemoveMembersRequest { - req := apiClient.RemoveMembers(ctx, model.GlobalFlagModel.ProjectId) + req := apiClient.RemoveMembers(ctx, model.ProjectId) payload := authorization.RemoveMembersPayload{ Members: utils.Ptr([]authorization.Member{ { diff --git a/internal/cmd/project/member/remove/remove_test.go b/internal/cmd/project/member/remove/remove_test.go index c3cb414e1..d0fc6d8f0 100644 --- a/internal/cmd/project/member/remove/remove_test.go +++ b/internal/cmd/project/member/remove/remove_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -137,54 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/project/project.go b/internal/cmd/project/project.go index f888fbbc5..c1a04db9a 100644 --- a/internal/cmd/project/project.go +++ b/internal/cmd/project/project.go @@ -3,7 +3,8 @@ package project import ( "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/project/create" "github.com/stackitcloud/stackit-cli/internal/cmd/project/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/project/describe" @@ -17,7 +18,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "project", Short: "Manages projects", @@ -32,7 +33,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/project/role/list/list.go b/internal/cmd/project/role/list/list.go index 2f49ba18b..292cad0d2 100644 --- a/internal/cmd/project/role/list/list.go +++ b/internal/cmd/project/role/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/authorization/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists roles and permissions of a project", @@ -50,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 roles and permissions of a project`, "$ stackit project role list --project-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -96,7 +96,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -115,42 +115,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *authorization.APIClient) authorization.ApiListRolesRequest { - return apiClient.ListRoles(ctx, projectResourceType, model.GlobalFlagModel.ProjectId) + return apiClient.ListRoles(ctx, projectResourceType, model.ProjectId) } func outputRolesResult(p *print.Printer, outputFormat string, roles []authorization.Role) error { - switch outputFormat { - case print.JSONOutputFormat: - // Show details - details, err := json.MarshalIndent(roles, "", " ") - if err != nil { - return fmt.Errorf("marshal roles: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(roles, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal roles: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, roles, func() error { table := tables.NewTable() table.SetHeader("ROLE NAME", "ROLE DESCRIPTION", "PERMISSION NAME", "PERMISSION DESCRIPTION") for i := range roles { @@ -173,5 +147,5 @@ func outputRolesResult(p *print.Printer, outputFormat string, roles []authorizat } return nil - } + }) } diff --git a/internal/cmd/project/role/list/list_test.go b/internal/cmd/project/role/list/list_test.go index b1f14f432..74d044fc2 100644 --- a/internal/cmd/project/role/list/list_test.go +++ b/internal/cmd/project/role/list/list_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -59,6 +59,7 @@ func fixtureRequest(mods ...func(request *authorization.ApiListRolesRequest)) au func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -99,48 +100,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -196,11 +156,10 @@ func Test_outputRolesResult(t *testing.T) { }, }}, false}, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputRolesResult(p, tt.args.outputFormat, tt.args.roles); (err != nil) != tt.wantErr { + if err := outputRolesResult(params.Printer, tt.args.outputFormat, tt.args.roles); (err != nil) != tt.wantErr { t.Errorf("outputRolesResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/project/role/role.go b/internal/cmd/project/role/role.go index bb0781f08..1c4c119a9 100644 --- a/internal/cmd/project/role/role.go +++ b/internal/cmd/project/role/role.go @@ -1,15 +1,15 @@ package role import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/project/role/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "role", Short: "Manages project roles", @@ -21,6 +21,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) } diff --git a/internal/cmd/project/update/update.go b/internal/cmd/project/update/update.go index b46641ec4..3c139b03c 100644 --- a/internal/cmd/project/update/update.go +++ b/internal/cmd/project/update/update.go @@ -5,7 +5,8 @@ import ( "fmt" "regexp" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/resourcemanager/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) const ( @@ -36,7 +37,7 @@ type inputModel struct { Labels *map[string]string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Updates a STACKIT project", @@ -54,9 +55,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Update the name of a STACKIT project by explicitly providing the project ID`, "$ stackit project update --name my-updated-project --project-id xxx"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -73,12 +74,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -102,7 +101,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a project. A label can be provided with the format key=value and the flag can be used multiple times to provide a list of labels") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -143,20 +142,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Labels: labels, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *resourcemanager.APIClient) resourcemanager.ApiPartialUpdateProjectRequest { - req := apiClient.PartialUpdateProject(ctx, model.ProjectId) + req := apiClient.DefaultAPI.PartialUpdateProject(ctx, model.ProjectId) req = req.PartialUpdateProjectPayload(resourcemanager.PartialUpdateProjectPayload{ ContainerParentId: model.ParentId, Name: model.Name, diff --git a/internal/cmd/project/update/update_test.go b/internal/cmd/project/update/update_test.go index 289a3b752..04338e8d8 100644 --- a/internal/cmd/project/update/update_test.go +++ b/internal/cmd/project/update/update_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +19,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &resourcemanager.APIClient{} +var testClient = &resourcemanager.APIClient{DefaultAPI: &resourcemanager.DefaultAPIService{}} var testProjectId = uuid.NewString() var testParentId = uuid.NewString() @@ -52,7 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *resourcemanager.ApiPartialUpdateProjectRequest)) resourcemanager.ApiPartialUpdateProjectRequest { - request := testClient.PartialUpdateProject(testCtx, testProjectId) + request := testClient.DefaultAPI.PartialUpdateProject(testCtx, testProjectId) request = request.PartialUpdateProjectPayload(resourcemanager.PartialUpdateProjectPayload{ ContainerParentId: utils.Ptr(testParentId), Name: utils.Ptr(nameFlag), @@ -66,6 +65,7 @@ func fixtureRequest(mods ...func(request *resourcemanager.ApiPartialUpdateProjec func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string labelValues []string isValid bool @@ -131,56 +131,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.labelValues { - err := cmd.Flags().Set(labelFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", labelFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + labelFlag: tt.labelValues, + }, tt.isValid) }) } } @@ -204,7 +157,7 @@ func TestBuildRequest(t *testing.T) { Verbosity: globalflags.VerbosityDefault, }, }, - expectedRequest: testClient.PartialUpdateProject(testCtx, testProjectId). + expectedRequest: testClient.DefaultAPI.PartialUpdateProject(testCtx, testProjectId). PartialUpdateProjectPayload(resourcemanager.PartialUpdateProjectPayload{}), }, } @@ -215,7 +168,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, resourcemanager.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/public-ip/associate/associate.go b/internal/cmd/public-ip/associate/associate.go index 82de08fcb..399228d1c 100644 --- a/internal/cmd/public-ip/associate/associate.go +++ b/internal/cmd/public-ip/associate/associate.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -31,7 +33,7 @@ type inputModel struct { AssociatedResourceId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("associate %s", publicIpIdArg), Short: "Associates a Public IP with a network interface or a virtual IP", @@ -56,7 +58,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.PublicIpId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId @@ -64,12 +66,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { publicIpLabel = model.PublicIpId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, *model.AssociatedResourceId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to associate public IP %q with resource %v?", publicIpLabel, *model.AssociatedResourceId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -79,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("associate public IP: %w", err) } - params.Printer.Outputf("Associated public IP %q with resource %v.\n", publicIpLabel, utils.PtrString(resp.GetNetworkInterface())) + params.Printer.Outputf("Associated public IP %q with resource %v.\n", publicIpLabel, resp.GetNetworkInterface()) return nil }, } @@ -108,23 +108,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu PublicIpId: publicIpId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdatePublicIPRequest { - req := apiClient.UpdatePublicIP(ctx, model.ProjectId, model.PublicIpId) + req := apiClient.DefaultAPI.UpdatePublicIP(ctx, model.ProjectId, model.Region, model.PublicIpId) payload := iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(model.AssociatedResourceId), + NetworkInterface: *iaas.NewNullableString(model.AssociatedResourceId), } return req.UpdatePublicIPPayload(payload) diff --git a/internal/cmd/public-ip/associate/associate_test.go b/internal/cmd/public-ip/associate/associate_test.go index bc2b890e7..3fe71e27d 100644 --- a/internal/cmd/public-ip/associate/associate_test.go +++ b/internal/cmd/public-ip/associate/associate_test.go @@ -7,19 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testPublicIpId = uuid.NewString() @@ -37,7 +40,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + associatedResourceIdFlag: testAssociatedResourceId, } for _, mod := range mods { @@ -51,6 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, PublicIpId: testPublicIpId, AssociatedResourceId: utils.Ptr(testAssociatedResourceId), @@ -62,7 +68,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.ApiUpdatePublicIPRequest { - request := testClient.UpdatePublicIP(testCtx, testProjectId, testPublicIpId) + request := testClient.DefaultAPI.UpdatePublicIP(testCtx, testProjectId, testRegion, testPublicIpId) request = request.UpdatePublicIPPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -72,7 +78,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.A func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { payload := iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), + NetworkInterface: *iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), } for _, mod := range mods { mod(&payload) @@ -105,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +127,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -165,8 +171,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -198,7 +204,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -236,7 +242,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { diff --git a/internal/cmd/public-ip/create/create.go b/internal/cmd/public-ip/create/create.go index 9d29b78d2..17b746e9a 100644 --- a/internal/cmd/public-ip/create/create.go +++ b/internal/cmd/public-ip/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -28,10 +28,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel AssociatedResourceId *string - Labels *map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Public IP", @@ -51,9 +51,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit public-ip create --associated-resource-id xxx --labels key=value,foo=bar`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,12 +72,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a public IP for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a public IP for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -99,7 +97,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringToString(labelFlag, nil, "Labels are key-value string pairs which can be attached to a public IP. E.g. '--labels key1=value1,key2=value2,...'") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -108,52 +106,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, AssociatedResourceId: flags.FlagToStringPointer(p, cmd, associatedResourceIdFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreatePublicIPRequest { - req := apiClient.CreatePublicIP(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreatePublicIP(ctx, model.ProjectId, model.Region) payload := iaas.CreatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(model.AssociatedResourceId), - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + NetworkInterface: *iaas.NewNullableString(model.AssociatedResourceId), + Labels: model.Labels, } return req.CreatePublicIPPayload(payload) } func outputResult(p *print.Printer, outputFormat, projectLabel string, publicIp iaas.PublicIp) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(publicIp, "", " ") - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(publicIp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, publicIp, func() error { p.Outputf("Created public IP for project %q.\nPublic IP ID: %s\n", projectLabel, utils.PtrString(publicIp.Id)) return nil - } + }) } diff --git a/internal/cmd/public-ip/create/create_test.go b/internal/cmd/public-ip/create/create_test.go index be2653938..3fda027bc 100644 --- a/internal/cmd/public-ip/create/create_test.go +++ b/internal/cmd/public-ip/create/create_test.go @@ -4,30 +4,34 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testAssociatedResourceId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + associatedResourceIdFlag: testAssociatedResourceId, labelFlag: "key=value", } @@ -42,11 +46,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, AssociatedResourceId: utils.Ptr(testAssociatedResourceId), - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -55,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreatePublicIPRequest)) iaas.ApiCreatePublicIPRequest { - request := testClient.CreatePublicIP(testCtx, testProjectId) + request := testClient.DefaultAPI.CreatePublicIP(testCtx, testProjectId, testRegion) request = request.CreatePublicIPPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -65,10 +70,10 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreatePublicIPRequest)) iaas.A func fixturePayload(mods ...func(payload *iaas.CreatePublicIPPayload)) iaas.CreatePublicIPPayload { payload := iaas.CreatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), - Labels: utils.Ptr(map[string]interface{}{ + NetworkInterface: *iaas.NewNullableString(utils.Ptr(testAssociatedResourceId)), + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(&payload) @@ -79,6 +84,7 @@ func fixturePayload(mods ...func(payload *iaas.CreatePublicIPPayload)) iaas.Crea func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,21 +126,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -142,46 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -205,7 +172,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { @@ -232,11 +199,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.publicIp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.publicIp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/public-ip/delete/delete.go b/internal/cmd/public-ip/delete/delete.go index 9cb01d38e..7e0fb8ee1 100644 --- a/internal/cmd/public-ip/delete/delete.go +++ b/internal/cmd/public-ip/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -26,7 +28,7 @@ type inputModel struct { PublicIpId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", publicIpIdArg), Short: "Deletes a Public IP", @@ -54,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.PublicIpId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId @@ -62,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { publicIpLabel = model.PublicIpId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete public IP %q? (This cannot be undone)", publicIpLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete public IP %q? (This cannot be undone)", publicIpLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -97,18 +97,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu PublicIpId: publicIpId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeletePublicIPRequest { - return apiClient.DeletePublicIP(ctx, model.ProjectId, model.PublicIpId) + return apiClient.DefaultAPI.DeletePublicIP(ctx, model.ProjectId, model.Region, model.PublicIpId) } diff --git a/internal/cmd/public-ip/delete/delete_test.go b/internal/cmd/public-ip/delete/delete_test.go index 667578fb0..5e54efb60 100644 --- a/internal/cmd/public-ip/delete/delete_test.go +++ b/internal/cmd/public-ip/delete/delete_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testPublicIpId = uuid.NewString() var testProjectId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, PublicIpId: testPublicIpId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeletePublicIPRequest)) iaas.ApiDeletePublicIPRequest { - request := testClient.DeletePublicIP(testCtx, testProjectId, testPublicIpId) + request := testClient.DefaultAPI.DeletePublicIP(testCtx, testProjectId, testRegion, testPublicIpId) for _, mod := range mods { mod(&request) } @@ -102,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/public-ip/describe/describe.go b/internal/cmd/public-ip/describe/describe.go index db2ac5050..125a54ff8 100644 --- a/internal/cmd/public-ip/describe/describe.go +++ b/internal/cmd/public-ip/describe/describe.go @@ -2,13 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -31,7 +30,7 @@ type inputModel struct { PublicIpId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", publicIpIdArg), Short: "Shows details of a Public IP", @@ -86,56 +85,29 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu PublicIpId: publicIpId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetPublicIPRequest { - return apiClient.GetPublicIP(ctx, model.ProjectId, model.PublicIpId) + return apiClient.DefaultAPI.GetPublicIP(ctx, model.ProjectId, model.Region, model.PublicIpId) } func outputResult(p *print.Printer, outputFormat string, publicIp iaas.PublicIp) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(publicIp, "", " ") - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(publicIp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, publicIp, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(publicIp.Id)) table.AddSeparator() table.AddRow("IP ADDRESS", utils.PtrString(publicIp.Ip)) table.AddSeparator() - if publicIp.NetworkInterface != nil { - networkInterfaceId := *publicIp.GetNetworkInterface() - table.AddRow("ASSOCIATED TO", networkInterfaceId) - table.AddSeparator() - } + networkInterfaceId := publicIp.GetNetworkInterface() + table.AddRow("ASSOCIATED TO", networkInterfaceId) + table.AddSeparator() - if publicIp.Labels != nil && len(*publicIp.Labels) > 0 { + if len(publicIp.Labels) > 0 { labels := []string{} - for key, value := range *publicIp.Labels { + for key, value := range publicIp.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -146,5 +118,5 @@ func outputResult(p *print.Printer, outputFormat string, publicIp iaas.PublicIp) return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/public-ip/describe/describe_test.go b/internal/cmd/public-ip/describe/describe_test.go index f69ba8dbb..a66125e0e 100644 --- a/internal/cmd/public-ip/describe/describe_test.go +++ b/internal/cmd/public-ip/describe/describe_test.go @@ -4,22 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testPublicIpId = uuid.NewString() @@ -35,7 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, PublicIpId: testPublicIpId, } @@ -58,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetPublicIPRequest)) iaas.ApiGetPublicIPRequest { - request := testClient.GetPublicIP(testCtx, testProjectId, testPublicIpId) + request := testClient.DefaultAPI.GetPublicIP(testCtx, testProjectId, testRegion, testPublicIpId) for _, mod := range mods { mod(&request) } @@ -102,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -234,11 +191,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.publicIp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.publicIp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/public-ip/disassociate/disassociate.go b/internal/cmd/public-ip/disassociate/disassociate.go index 7630ce7c5..08d432e68 100644 --- a/internal/cmd/public-ip/disassociate/disassociate.go +++ b/internal/cmd/public-ip/disassociate/disassociate.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -13,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -27,7 +29,7 @@ type inputModel struct { PublicIpId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("disassociate %s", publicIpIdArg), Short: "Disassociates a Public IP from a network interface or a virtual IP", @@ -52,7 +54,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - publicIpLabel, associatedResourceId, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, associatedResourceId, err := iaasUtils.GetPublicIP(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.PublicIpId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId @@ -60,12 +62,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { publicIpLabel = model.PublicIpId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource %q?", publicIpLabel, associatedResourceId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disassociate public IP %q from the associated resource %q?", publicIpLabel, associatedResourceId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -95,23 +95,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu PublicIpId: publicIpId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdatePublicIPRequest { - req := apiClient.UpdatePublicIP(ctx, model.ProjectId, model.PublicIpId) + req := apiClient.DefaultAPI.UpdatePublicIP(ctx, model.ProjectId, model.Region, model.PublicIpId) payload := iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(nil), + NetworkInterface: *iaas.NewNullableString(nil), } return req.UpdatePublicIPPayload(payload) diff --git a/internal/cmd/public-ip/disassociate/disassociate_test.go b/internal/cmd/public-ip/disassociate/disassociate_test.go index 956be6b23..73cb88d5c 100644 --- a/internal/cmd/public-ip/disassociate/disassociate_test.go +++ b/internal/cmd/public-ip/disassociate/disassociate_test.go @@ -4,22 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testPublicIpId = uuid.NewString() @@ -36,7 +38,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -49,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, PublicIpId: testPublicIpId, } @@ -59,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.ApiUpdatePublicIPRequest { - request := testClient.UpdatePublicIP(testCtx, testProjectId, testPublicIpId) + request := testClient.DefaultAPI.UpdatePublicIP(testCtx, testProjectId, testRegion, testPublicIpId) request = request.UpdatePublicIPPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -69,7 +73,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.A func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { payload := iaas.UpdatePublicIPPayload{ - NetworkInterface: iaas.NewNullableString(nil), + NetworkInterface: *iaas.NewNullableString(nil), } for _, mod := range mods { mod(&payload) @@ -102,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,8 +142,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -171,7 +175,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -209,7 +213,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { diff --git a/internal/cmd/public-ip/list/list.go b/internal/cmd/public-ip/list/list.go index 37756c8c2..884613390 100644 --- a/internal/cmd/public-ip/list/list.go +++ b/internal/cmd/public-ip/list/list.go @@ -2,11 +2,12 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -33,7 +33,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Public IPs of a project", @@ -57,9 +57,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit public-ip list --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -77,25 +77,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list public IPs: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId - } - params.Printer.Info("No public IPs found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -107,7 +104,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -127,20 +124,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListPublicIPsRequest { - req := apiClient.ListPublicIPs(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListPublicIPs(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } @@ -148,39 +137,25 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, publicIps []iaas.PublicIp) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(publicIps, "", " ") - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, publicIps []iaas.PublicIp) error { + return p.OutputResult(outputFormat, publicIps, func() error { + if len(publicIps) == 0 { + p.Outputf("No public IPs found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(publicIps, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "IP ADDRESS", "USED BY") for _, publicIp := range publicIps { - networkInterfaceId := utils.PtrStringDefault(publicIp.GetNetworkInterface(), "") table.AddRow( utils.PtrString(publicIp.Id), utils.PtrString(publicIp.Ip), - networkInterfaceId, + publicIp.GetNetworkInterface(), ) table.AddSeparator() } p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/public-ip/list/list_test.go b/internal/cmd/public-ip/list/list_test.go index a6fe99a48..e44b2fcc2 100644 --- a/internal/cmd/public-ip/list/list_test.go +++ b/internal/cmd/public-ip/list/list_test.go @@ -4,29 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testLabelSelector = "label" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", labelSelectorFlag: testLabelSelector, } @@ -41,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), @@ -52,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListPublicIPsRequest)) iaas.ApiListPublicIPsRequest { - request := testClient.ListPublicIPs(testCtx, testProjectId) + request := testClient.DefaultAPI.ListPublicIPs(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -63,6 +68,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListPublicIPsRequest)) iaas.Ap func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -86,21 +92,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -132,46 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -195,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -207,6 +174,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string publicIps []iaas.PublicIp } tests := []struct { @@ -220,11 +188,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.publicIps); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.publicIps); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/public-ip/public-ip.go b/internal/cmd/public-ip/public-ip.go index 3be1ac1c4..77a4e3a2b 100644 --- a/internal/cmd/public-ip/public-ip.go +++ b/internal/cmd/public-ip/public-ip.go @@ -1,21 +1,22 @@ package publicip import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/associate" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/create" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/disassociate" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/ranges" "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "public-ip", Short: "Provides functionality for public IPs", @@ -27,7 +28,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) @@ -35,4 +36,5 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(associate.NewCmd(params)) cmd.AddCommand(disassociate.NewCmd(params)) + cmd.AddCommand(ranges.NewCmd(params)) } diff --git a/internal/cmd/public-ip/ranges/list/list.go b/internal/cmd/public-ip/ranges/list/list.go new file mode 100644 index 000000000..05d09a907 --- /dev/null +++ b/internal/cmd/public-ip/ranges/list/list.go @@ -0,0 +1,123 @@ +package list + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" +) + +const ( + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Limit *int64 +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all STACKIT public-ip ranges", + Long: "Lists all STACKIT public-ip ranges.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Lists all STACKIT public-ip ranges`, + "$ stackit public-ip ranges list", + ), + examples.NewExample( + `Lists all STACKIT public-ip ranges, piping to a tool like fzf for interactive selection`, + "$ stackit public-ip ranges list -o pretty | fzf", + ), + examples.NewExample( + `Lists up to 10 STACKIT public-ip ranges`, + "$ stackit public-ip ranges list --limit 10", + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := apiClient.DefaultAPI.ListPublicIPRanges(ctx) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list public IP ranges: %w", err) + } + publicIpRanges := resp.Items + + // Truncate output + if model.Limit != nil && len(publicIpRanges) > int(*model.Limit) { + publicIpRanges = publicIpRanges[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, publicIpRanges) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + Limit: limit, + } + + p.DebugInputModel(model) + return &model, nil +} + +func outputResult(p *print.Printer, outputFormat string, publicIpRanges []iaas.PublicNetwork) error { + return p.OutputResult(outputFormat, publicIpRanges, func() error { + if len(publicIpRanges) == 0 { + p.Outputln("No public IP ranges found") + return nil + } + + for _, item := range publicIpRanges { + if item.Cidr != "" { + p.Outputln(item.Cidr) + } + } + + return nil + }) +} diff --git a/internal/cmd/public-ip/ranges/list/list_test.go b/internal/cmd/public-ip/ranges/list/list_test.go new file mode 100644 index 000000000..8dfe478a1 --- /dev/null +++ b/internal/cmd/public-ip/ranges/list/list_test.go @@ -0,0 +1,189 @@ +package list + +import ( + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func TestParseInput(t *testing.T) { + projectId := uuid.New().String() + tests := []struct { + description string + argValues []string + flagValues map[string]string + expectedModel *inputModel + isValid bool + }{ + { + description: "valid project id", + flagValues: map[string]string{ + "project-id": projectId, + }, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: projectId, + Verbosity: globalflags.InfoVerbosity, + }, + }, + isValid: true, + }, + { + description: "missing project id does not lead into error", + flagValues: map[string]string{}, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.InfoVerbosity, + }, + }, + isValid: true, + }, + { + description: "valid input with limit", + flagValues: map[string]string{ + "limit": "10", + }, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.InfoVerbosity, + }, + Limit: utils.Ptr(int64(10)), + }, + isValid: true, + }, + { + description: "valid input without limit", + flagValues: map[string]string{}, + expectedModel: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.InfoVerbosity, + }, + }, + isValid: true, + }, + { + description: "invalid limit (zero)", + flagValues: map[string]string{ + "limit": "0", + }, + expectedModel: nil, + isValid: false, + }, + { + description: "invalid limit (negative)", + flagValues: map[string]string{ + "limit": "-1", + }, + expectedModel: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestOutputResult(t *testing.T) { + tests := []struct { + name string + outputFormat string + publicIpRanges []iaas.PublicNetwork + expectedOutput string + wantErr bool + }{ + { + name: "JSON output single", + outputFormat: "json", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + }, + wantErr: false, + }, + { + name: "JSON output multiple", + outputFormat: "json", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + {Cidr: "192.167.0.0/24"}, + }, + wantErr: false, + }, + { + name: "YAML output single", + outputFormat: "yaml", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + }, + wantErr: false, + }, + { + name: "YAML output multiple", + outputFormat: "yaml", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + {Cidr: "192.167.0.0/24"}, + }, + wantErr: false, + }, + { + name: "pretty output single", + outputFormat: "pretty", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + }, + wantErr: false, + }, + { + name: "pretty output multiple", + outputFormat: "pretty", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + {Cidr: "192.167.0.0/24"}, + }, + wantErr: false, + }, + { + name: "default output", + outputFormat: "", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: "192.168.0.0/24"}, + }, + wantErr: false, + }, + { + name: "empty list", + outputFormat: "json", + publicIpRanges: []iaas.PublicNetwork{}, + wantErr: false, + }, + { + name: "nil CIDR", + outputFormat: "pretty", + publicIpRanges: []iaas.PublicNetwork{ + {Cidr: ""}, + {Cidr: "192.168.0.0/24"}, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + err := outputResult(params.Printer, tt.outputFormat, tt.publicIpRanges) + if (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/public-ip/ranges/ranges.go b/internal/cmd/public-ip/ranges/ranges.go new file mode 100644 index 000000000..5978bbbb1 --- /dev/null +++ b/internal/cmd/public-ip/ranges/ranges.go @@ -0,0 +1,26 @@ +package ranges + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/public-ip/ranges/list" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "ranges", + Short: "Provides functionality for STACKIT public-ip ranges", + Long: "Provides functionality for STACKIT public-ip ranges", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(list.NewCmd(params)) +} diff --git a/internal/cmd/public-ip/update/update.go b/internal/cmd/public-ip/update/update.go index 78ae5d13e..5e99f8f25 100644 --- a/internal/cmd/public-ip/update/update.go +++ b/internal/cmd/public-ip/update/update.go @@ -2,15 +2,14 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -19,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -31,10 +29,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel PublicIpId string - Labels *map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", publicIpIdArg), Short: "Updates a Public IP", @@ -63,18 +61,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.PublicIpId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get public IP: %v", err) publicIpLabel = model.PublicIpId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update public IP %q?", publicIpLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update public IP %q?", publicIpLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -103,10 +99,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return nil, &cliErr.ProjectIdError{} } - labels := flags.FlagToStringToStringPointer(p, cmd, labelFlag) + labels := flags.FlagToStringToAny(p, cmd, labelFlag) if labels == nil { - return nil, &errors.EmptyUpdateError{} + return nil, &cliErr.EmptyUpdateError{} } model := inputModel{ @@ -115,48 +111,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Labels: labels, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdatePublicIPRequest { - req := apiClient.UpdatePublicIP(ctx, model.ProjectId, model.PublicIpId) + req := apiClient.DefaultAPI.UpdatePublicIP(ctx, model.ProjectId, model.Region, model.PublicIpId) payload := iaas.UpdatePublicIPPayload{ - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, } return req.UpdatePublicIPPayload(payload) } func outputResult(p *print.Printer, model *inputModel, publicIpLabel string, publicIp *iaas.PublicIp) error { - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(publicIp, "", " ") - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(publicIp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal public IP: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, publicIp, func() error { p.Outputf("Updated public IP %q.\n", publicIpLabel) return nil - } + }) } diff --git a/internal/cmd/public-ip/update/update_test.go b/internal/cmd/public-ip/update/update_test.go index 87c598665..50187f733 100644 --- a/internal/cmd/public-ip/update/update_test.go +++ b/internal/cmd/public-ip/update/update_test.go @@ -4,23 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testPublicIpId = uuid.NewString() @@ -37,8 +37,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - labelFlag: "key=value", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -51,11 +53,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, PublicIpId: testPublicIpId, - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -64,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.ApiUpdatePublicIPRequest { - request := testClient.UpdatePublicIP(testCtx, testProjectId, testPublicIpId) + request := testClient.DefaultAPI.UpdatePublicIP(testCtx, testProjectId, testRegion, testPublicIpId) request = request.UpdatePublicIPPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -74,9 +77,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdatePublicIPRequest)) iaas.A func fixturePayload(mods ...func(payload *iaas.UpdatePublicIPPayload)) iaas.UpdatePublicIPPayload { payload := iaas.UpdatePublicIPPayload{ - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(&payload) @@ -109,7 +112,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -117,7 +120,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -125,7 +128,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -145,8 +148,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -178,7 +181,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -216,7 +219,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { diff --git a/internal/cmd/quota/list/list.go b/internal/cmd/quota/list/list.go index 81397affc..e26c9038c 100644 --- a/internal/cmd/quota/list/list.go +++ b/internal/cmd/quota/list/list.go @@ -2,29 +2,28 @@ package list import ( "context" - "encoding/json" "fmt" "strconv" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists quotas", @@ -36,9 +35,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit quota list`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -49,14 +48,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId - } - // Call API request := buildRequest(ctx, model, apiClient) @@ -65,22 +56,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list quotas: %w", err) } - if items := response.Quotas; items == nil { - params.Printer.Info("No quotas found for project %q", projectLabel) - } else { - if err := outputResult(params.Printer, model.OutputFormat, items); err != nil { - return fmt.Errorf("output quotas: %w", err) - } - } - - return nil + return outputResult(params.Printer, model.OutputFormat, &response.Quotas) }, } return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -90,109 +73,47 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListQuotasRequest { - request := apiClient.ListQuotas(ctx, model.ProjectId) + request := apiClient.DefaultAPI.ListQuotas(ctx, model.ProjectId, model.Region) return request } func outputResult(p *print.Printer, outputFormat string, quotas *iaas.QuotaList) error { - if quotas == nil { - return fmt.Errorf("quotas is nil") - } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(quotas, "", " ") - if err != nil { - return fmt.Errorf("marshal quota list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(quotas, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal quota list: %w", err) - } - p.Outputln(string(details)) - - return nil - - default: + return p.OutputResult(outputFormat, quotas, func() error { table := tables.NewTable() table.SetHeader("NAME", "LIMIT", "CURRENT USAGE", "PERCENT") - if val := quotas.BackupGigabytes; val != nil { - table.AddRow("Total size in GiB of backups [GiB]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Backups; val != nil { - table.AddRow("Number of backups [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Gigabytes; val != nil { - table.AddRow("Total size in GiB of volumes and snapshots [GiB]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Networks; val != nil { - table.AddRow("Number of networks [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Nics; val != nil { - table.AddRow("Number of network interfaces (nics) [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.PublicIps; val != nil { - table.AddRow("Number of public IP addresses [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Ram; val != nil { - table.AddRow("Amount of server RAM in MiB [MiB]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.SecurityGroupRules; val != nil { - table.AddRow("Number of security group rules [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.SecurityGroups; val != nil { - table.AddRow("Number of security groups [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Snapshots; val != nil { - table.AddRow("Number of snapshots [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Vcpu; val != nil { - table.AddRow("Number of server cores (vcpu) [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } - if val := quotas.Volumes; val != nil { - table.AddRow("Number of volumes [Count]", conv(val.Limit), conv(val.Usage), percentage(val)) - } + table.AddRow(quotaRow("Total size in GiB of backups [GiB]", quotas.BackupGigabytes)...) + table.AddRow(quotaRow("Number of backups [Count]", quotas.Backups)...) + table.AddRow(quotaRow("Total size in GiB of volumes and snapshots [GiB]", quotas.Gigabytes)...) + table.AddRow(quotaRow("Number of networks [Count]", quotas.Networks)...) + table.AddRow(quotaRow("Number of network interfaces (nics) [Count]", quotas.Nics)...) + table.AddRow(quotaRow("Number of public IP addresses [Count]", quotas.PublicIps)...) + table.AddRow(quotaRow("Amount of server RAM in MiB [MiB]", quotas.Ram)...) + table.AddRow(quotaRow("Number of security group rules [Count]", quotas.SecurityGroupRules)...) + table.AddRow(quotaRow("Number of security groups [Count]", quotas.SecurityGroups)...) + table.AddRow(quotaRow("Number of snapshots [Count]", quotas.Snapshots)...) + table.AddRow(quotaRow("Number of server cores (vcpu) [Count]", quotas.Vcpu)...) + table.AddRow(quotaRow("Number of volumes [Count]", quotas.Volumes)...) err := table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) } return nil - } + }) } -func conv(n *int64) string { - if n != nil { - return strconv.FormatInt(*n, 10) - } - return "n/a" +func quotaRow(description string, quota iaas.Quota) []interface{} { + result := make([]interface{}, 0, 4) + result = append(result, description, conv(quota.Limit), conv(quota.Usage), fmt.Sprintf("%3.1f%%", 100.0/float64(quota.Limit)*float64(quota.Usage))) + return result } -func percentage(val interface { - GetLimitOk() (int64, bool) - GetUsageOk() (int64, bool) -}) string { - a, aOk := val.GetLimitOk() - b, bOk := val.GetUsageOk() - if aOk && bOk { - return fmt.Sprintf("%3.1f%%", 100.0/float64(a)*float64(b)) - } - return "n/a" +func conv(n int64) string { + return strconv.FormatInt(n, 10) } diff --git a/internal/cmd/quota/list/list_test.go b/internal/cmd/quota/list/list_test.go index ab2cdd8ea..cad93beb6 100644 --- a/internal/cmd/quota/list/list_test.go +++ b/internal/cmd/quota/list/list_test.go @@ -4,29 +4,32 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -36,7 +39,11 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, } for _, mod := range mods { mod(model) @@ -45,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListQuotasRequest)) iaas.ApiListQuotasRequest { - request := testClient.ListQuotas(testCtx, testProjectId) + request := testClient.DefaultAPI.ListQuotas(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -55,6 +62,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListQuotasRequest)) iaas.ApiLi func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -73,21 +81,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -95,44 +103,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -155,7 +126,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -167,7 +138,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string - quotas *iaas.QuotaList + quotas iaas.QuotaList } tests := []struct { name string @@ -177,21 +148,20 @@ func Test_outputResult(t *testing.T) { { name: "empty", args: args{}, - wantErr: true, + wantErr: false, }, { name: "set quota empty", args: args{ - quotas: &iaas.QuotaList{}, + quotas: iaas.QuotaList{}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.quotas); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, &tt.args.quotas); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/quota/quota.go b/internal/cmd/quota/quota.go index ff323e97e..ed65097d2 100644 --- a/internal/cmd/quota/quota.go +++ b/internal/cmd/quota/quota.go @@ -1,16 +1,16 @@ package quota import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/quota/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "quota", Short: "Manage server quotas", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( list.NewCmd(params), ) diff --git a/internal/cmd/rabbitmq/credentials/create/create.go b/internal/cmd/rabbitmq/credentials/create/create.go index 6195ec768..aceb05727 100644 --- a/internal/cmd/rabbitmq/credentials/create/create.go +++ b/internal/cmd/rabbitmq/credentials/create/create.go @@ -2,12 +2,14 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/client" rabbitmqUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { ShowPassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for a RabbitMQ instance", @@ -45,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for a RabbitMQ instance and show the password in the output`, "$ stackit rabbitmq credentials create --instance-id xxx --show-password"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -58,18 +59,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -94,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -106,20 +105,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ShowPassword: flags.FlagToBoolValue(p, cmd, showPasswordFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.Region, model.InstanceId) return req } @@ -133,45 +124,27 @@ func outputResult(p *print.Printer, model inputModel, instanceLabel string, resp if !model.ShowPassword { if resp.Raw == nil { - resp.Raw = &rabbitmq.RawCredentials{Credentials: &rabbitmq.Credentials{}} - } else if resp.Raw.Credentials == nil { - resp.Raw.Credentials = &rabbitmq.Credentials{} + resp.Raw = &rabbitmq.RawCredentials{Credentials: rabbitmq.Credentials{}} } - resp.Raw.Credentials.Password = utils.Ptr("hidden") + resp.Raw.Credentials.Password = "hidden" } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ credentials: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, utils.PtrString(resp.Id)) + return p.OutputResult(model.OutputFormat, resp, func() error { + p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, resp.Id) // The username field cannot be set by the user so we only display it if it's not returned empty - if resp.HasRaw() && resp.Raw.Credentials != nil { - if username := resp.Raw.Credentials.Username; username != nil && *username != "" { - p.Outputf("Username: %s\n", *username) + if resp.HasRaw() { + if username := resp.Raw.Credentials.Username; username != "" { + p.Outputf("Username: %s\n", username) } if !model.ShowPassword { p.Outputf("Password: \n") } else { - p.Outputf("Password: %s\n", utils.PtrString(resp.Raw.Credentials.Password)) + p.Outputf("Password: %s\n", resp.Raw.Credentials.Password) } - p.Outputf("Host: %s\n", utils.PtrString(resp.Raw.Credentials.Host)) + p.Outputf("Host: %s\n", resp.Raw.Credentials.Host) p.Outputf("Port: %s\n", utils.PtrString(resp.Raw.Credentials.Port)) } - p.Outputf("URI: %s\n", utils.PtrString(resp.Uri)) + p.Outputf("URI: %s\n", resp.Uri) return nil - } + }) } diff --git a/internal/cmd/rabbitmq/credentials/create/create_test.go b/internal/cmd/rabbitmq/credentials/create/create_test.go index 84e65eaa0..90703282e 100644 --- a/internal/cmd/rabbitmq/credentials/create/create_test.go +++ b/internal/cmd/rabbitmq/credentials/create/create_test.go @@ -7,25 +7,26 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, } for _, mod := range mods { mod(flagValues) @@ -37,6 +38,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -48,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiCreateCredentialsRequest)) rabbitmq.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -58,6 +60,7 @@ func fixtureRequest(mods ...func(request *rabbitmq.ApiCreateCredentialsRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -86,21 +89,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -129,46 +132,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -192,7 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -243,11 +207,10 @@ func Test_outputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/credentials/credentials.go b/internal/cmd/rabbitmq/credentials/credentials.go index 80c06fb8e..2f7c435e2 100644 --- a/internal/cmd/rabbitmq/credentials/credentials.go +++ b/internal/cmd/rabbitmq/credentials/credentials.go @@ -1,18 +1,18 @@ package credentials import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/credentials/create" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/credentials/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for RabbitMQ credentials", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/rabbitmq/credentials/delete/delete.go b/internal/cmd/rabbitmq/credentials/delete/delete.go index 9421e898e..8defa0092 100644 --- a/internal/cmd/rabbitmq/credentials/delete/delete.go +++ b/internal/cmd/rabbitmq/credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsIdArg), Short: "Deletes credentials of a RabbitMQ instance", @@ -55,24 +56,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - credentialsLabel, err := rabbitmqUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialsLabel, err := rabbitmqUtils.GetCredentialsUsername(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials user name: %v", err) credentialsLabel = model.CredentialsId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,19 +110,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) return req } diff --git a/internal/cmd/rabbitmq/credentials/delete/delete_test.go b/internal/cmd/rabbitmq/credentials/delete/delete_test.go index e716f7c72..4e31acc35 100644 --- a/internal/cmd/rabbitmq/credentials/delete/delete_test.go +++ b/internal/cmd/rabbitmq/credentials/delete/delete_test.go @@ -4,23 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() @@ -36,8 +34,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, } for _, mod := range mods { mod(flagValues) @@ -49,6 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -61,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiDeleteCredentialsRequest)) rabbitmq.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testRegion, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -105,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -165,54 +165,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +189,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/rabbitmq/credentials/describe/describe.go b/internal/cmd/rabbitmq/credentials/describe/describe.go index e04eb1c2e..820560667 100644 --- a/internal/cmd/rabbitmq/credentials/describe/describe.go +++ b/internal/cmd/rabbitmq/credentials/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialsIdArg), Short: "Shows details of credentials of a RabbitMQ instance", @@ -95,20 +94,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiGetCredentialsRequest { - req := apiClient.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) return req } @@ -116,34 +107,18 @@ func outputResult(p *print.Printer, outputFormat string, credentials *rabbitmq.C if credentials == nil { return fmt.Errorf("no response passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ credentials: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, credentials, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(credentials.Id)) + table.AddRow("ID", credentials.Id) table.AddSeparator() // The username field cannot be set by the user so we only display it if it's not returned empty - if credentials.HasRaw() && credentials.Raw.Credentials != nil { - if username := credentials.Raw.Credentials.Username; username != nil && *username != "" { - table.AddRow("USERNAME", *username) + if credentials.HasRaw() { + if username := credentials.Raw.Credentials.Username; username != "" { + table.AddRow("USERNAME", username) table.AddSeparator() } - table.AddRow("PASSWORD", utils.PtrString(credentials.Raw.Credentials.Password)) + table.AddRow("PASSWORD", credentials.Raw.Credentials.Password) table.AddSeparator() table.AddRow("URI", utils.PtrString(credentials.Raw.Credentials.Uri)) } @@ -153,5 +128,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials *rabbitmq.C } return nil - } + }) } diff --git a/internal/cmd/rabbitmq/credentials/describe/describe_test.go b/internal/cmd/rabbitmq/credentials/describe/describe_test.go index a40cd610d..580ec41ff 100644 --- a/internal/cmd/rabbitmq/credentials/describe/describe_test.go +++ b/internal/cmd/rabbitmq/credentials/describe/describe_test.go @@ -7,19 +7,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() @@ -35,8 +35,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, } for _, mod := range mods { mod(flagValues) @@ -48,6 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -60,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiGetCredentialsRequest)) rabbitmq.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testRegion, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -104,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -112,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -120,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -164,54 +166,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -235,7 +190,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -270,11 +225,10 @@ func Test_outputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/credentials/list/list.go b/internal/cmd/rabbitmq/credentials/list/list.go index d31efe256..1a3591147 100644 --- a/internal/cmd/rabbitmq/credentials/list/list.go +++ b/internal/cmd/rabbitmq/credentials/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/client" rabbitmqUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials' IDs for a RabbitMQ instance", @@ -49,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials' IDs for a RabbitMQ instance`, "$ stackit rabbitmq credentials list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,22 +67,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list RabbitMQ credentials: %w", err) } - credentials := *resp.CredentialsList - if len(credentials) == 0 { - instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId - } - params.Printer.Info("No credentials found for instance %q\n", instanceLabel) - return nil + credentials := resp.GetCredentialsList() + + instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { credentials = credentials[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, credentials) + + return outputResult(params.Printer, model.OutputFormat, instanceLabel, credentials) }, } configureFlags(cmd) @@ -98,7 +95,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,47 +115,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.Region, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []rabbitmq.CredentialsListItem) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, credentials []rabbitmq.CredentialsListItem) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("ID") for i := range credentials { c := credentials[i] - table.AddRow(utils.PtrString(c.Id)) + table.AddRow(c.Id) } err := table.Display(p) if err != nil { @@ -166,5 +143,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials []rabbitmq. } return nil - } + }) } diff --git a/internal/cmd/rabbitmq/credentials/list/list_test.go b/internal/cmd/rabbitmq/credentials/list/list_test.go index 77f7f2f5e..65fc5d867 100644 --- a/internal/cmd/rabbitmq/credentials/list/list_test.go +++ b/internal/cmd/rabbitmq/credentials/list/list_test.go @@ -7,27 +7,28 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" var testInstanceId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceIdFlag: testInstanceId, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -39,6 +40,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -51,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiListCredentialsRequest)) rabbitmq.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -61,6 +63,7 @@ func fixtureRequest(mods ...func(request *rabbitmq.ApiListCredentialsRequest)) r func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -79,21 +82,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -136,46 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -210,8 +174,9 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { - outputFormat string - credentials []rabbitmq.CredentialsListItem + outputFormat string + instanceLabel string + credentials []rabbitmq.CredentialsListItem } tests := []struct { name string @@ -234,11 +199,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/instance/create/create.go b/internal/cmd/rabbitmq/instance/create/create.go index a5ea1b7af..8be9c6a37 100644 --- a/internal/cmd/rabbitmq/instance/create/create.go +++ b/internal/cmd/rabbitmq/instance/create/create.go @@ -2,13 +2,12 @@ package create import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +21,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api/wait" ) const ( @@ -33,7 +32,6 @@ const ( metricsFrequencyFlag = "metrics-frequency" metricsPrefixFlag = "metrics-prefix" monitoringInstanceIdFlag = "monitoring-instance-id" - pluginFlag = "plugin" sgwAclFlag = "acl" syslogFlag = "syslog" planIdFlag = "plan-id" @@ -41,24 +39,30 @@ const ( versionFlag = "version" ) +var flagPlugins = flags.StringEnumSliceFlag( + "plugin", + rabbitmq.AllowedInstanceParametersPluginsInnerEnumValues, + "Plugins", +) + type inputModel struct { *globalflags.GlobalFlagModel PlanName string Version string - InstanceName *string + InstanceName string EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string - Plugin *[]string + Plugin []rabbitmq.InstanceParametersPluginsInner SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a RabbitMQ instance", @@ -75,9 +79,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a RabbitMQ instance with name "my-instance" and specify IP range which is allowed to access it`, "$ stackit rabbitmq instance create --name my-instance --plan-id xxx --acl 1.2.3.0/24"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -94,16 +98,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create an RabbitMQ instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create an RabbitMQ instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -115,17 +117,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create RabbitMQ instance: %w", err) } - instanceId := *resp.InstanceId + instanceId := resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for RabbitMQ instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, instanceId, resp) @@ -139,10 +141,10 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") - cmd.Flags().StringSlice(pluginFlag, []string{}, "Plugin") + flagPlugins.Register(cmd) cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().StringSlice(syslogFlag, []string{}, "Syslog") cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") @@ -153,7 +155,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -176,50 +178,42 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), EnableMonitoring: flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag), MonitoringInstanceId: flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag), Graphite: flags.FlagToStringPointer(p, cmd, graphiteFlag), - MetricsFrequency: flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag), + MetricsFrequency: flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag), MetricsPrefix: flags.FlagToStringPointer(p, cmd, metricsPrefixFlag), - Plugin: flags.FlagToStringSlicePointer(p, cmd, pluginFlag), + Plugin: flagPlugins.Get(), SgwAcl: flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag), - Syslog: flags.FlagToStringSlicePointer(p, cmd, syslogFlag), + Syslog: flags.FlagToStringSliceValue(p, cmd, syslogFlag), PlanId: planId, PlanName: planName, Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } type rabbitMQClient interface { - CreateInstance(ctx context.Context, projectId string) rabbitmq.ApiCreateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*rabbitmq.ListOfferingsResponse, error) + CreateInstance(ctx context.Context, projectId, region string) rabbitmq.ApiCreateInstanceRequest + ListOfferings(ctx context.Context, projectId, region string) rabbitmq.ApiListOfferingsRequest } func buildRequest(ctx context.Context, model *inputModel, apiClient rabbitMQClient) (rabbitmq.ApiCreateInstanceRequest, error) { - req := apiClient.CreateInstance(ctx, model.ProjectId) + req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) - var planId *string + var planId string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get RabbitMQ offerings: %w", err) } if model.PlanId == nil { - planId, err = rabbitmqUtils.LoadPlanId(model.PlanName, model.Version, offerings) + foundPlanId, err := rabbitmqUtils.LoadPlanId(model.PlanName, model.Version, offerings) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -227,12 +221,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient rabbitMQClie } return req, err } + planId = *foundPlanId } else { err := rabbitmqUtils.ValidatePlanId(*model.PlanId, offerings) if err != nil { return req, err } - planId = model.PlanId + planId = *model.PlanId } var sgwAcl *string @@ -268,29 +263,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId return fmt.Errorf("no response passed") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, instanceId) return nil - } + }) } diff --git a/internal/cmd/rabbitmq/instance/create/create_test.go b/internal/cmd/rabbitmq/instance/create/create_test.go index aa39a62e8..e14e3a0b6 100644 --- a/internal/cmd/rabbitmq/instance/create/create_test.go +++ b/internal/cmd/rabbitmq/instance/create/create_test.go @@ -5,56 +5,60 @@ import ( "fmt" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} -type rabbitMQClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *rabbitmq.ListOfferingsResponse } -func (c *rabbitMQClientMocked) CreateInstance(ctx context.Context, projectId string) rabbitmq.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId) -} - -func (c *rabbitMQClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*rabbitmq.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(settings mockSettings) rabbitmq.DefaultAPI { + return rabbitmq.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ rabbitmq.ApiListOfferingsRequest) (*rabbitmq.ListOfferingsResponse, error) { + if settings.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return settings.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var testProjectId = uuid.NewString() var testPlanId = uuid.NewString() var testMonitoringInstanceId = uuid.NewString() +var testInstanceName = "instance" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceNameFlag: "example-name", - enableMonitoringFlag: "true", - graphiteFlag: "example-graphite", - metricsFrequencyFlag: "100", - metricsPrefixFlag: "example-prefix", - monitoringInstanceIdFlag: testMonitoringInstanceId, - pluginFlag: "example-plugin", - sgwAclFlag: "198.51.100.14/24", - syslogFlag: "example-syslog", - planIdFlag: testPlanId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + instanceNameFlag: "example-name", + enableMonitoringFlag: "true", + graphiteFlag: "example-graphite", + metricsFrequencyFlag: "100", + metricsPrefixFlag: "example-prefix", + monitoringInstanceIdFlag: testMonitoringInstanceId, + flagPlugins.Name(): string(rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT), + sgwAclFlag: "198.51.100.14/24", + syslogFlag: "example-syslog", + planIdFlag: testPlanId, } for _, mod := range mods { mod(flagValues) @@ -66,17 +70,18 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugin: utils.Ptr([]string{"example-plugin"}), + Plugin: []rabbitmq.InstanceParametersPluginsInner{rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT}, SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -86,20 +91,20 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiCreateInstanceRequest)) rabbitmq.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion) request = request.CreateInstancePayload(rabbitmq.CreateInstancePayload{ - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", Parameters: &rabbitmq.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugins: utils.Ptr([]string{"example-plugin"}), + Plugins: []rabbitmq.InstanceParametersPluginsInner{rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT}, SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }) for _, mod := range mods { mod(&request) @@ -110,6 +115,7 @@ func fixtureRequest(mods ...func(request *rabbitmq.ApiCreateInstanceRequest)) ra func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string sgwAclValues []string pluginValues []string @@ -145,9 +151,9 @@ func TestParseInput(t *testing.T) { { description: "required fields only", flagValues: map[string]string{ - projectIdFlag: testProjectId, - instanceNameFlag: "example-name", - planIdFlag: testPlanId, + globalflags.ProjectIdFlag: testProjectId, + instanceNameFlag: "example-name", + planIdFlag: testPlanId, }, isValid: true, expectedModel: &inputModel{ @@ -155,20 +161,20 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", PlanId: utils.Ptr(testPlanId), }, }, { description: "zero values", flagValues: map[string]string{ - projectIdFlag: testProjectId, - planIdFlag: testPlanId, - instanceNameFlag: "", - enableMonitoringFlag: "false", - graphiteFlag: "", - metricsFrequencyFlag: "0", - metricsPrefixFlag: "", + globalflags.ProjectIdFlag: testProjectId, + planIdFlag: testPlanId, + instanceNameFlag: "", + enableMonitoringFlag: "false", + graphiteFlag: "", + metricsFrequencyFlag: "0", + metricsPrefixFlag: "", }, isValid: true, expectedModel: &inputModel{ @@ -177,31 +183,31 @@ func TestParseInput(t *testing.T) { Verbosity: globalflags.VerbosityDefault, }, PlanId: utils.Ptr(testPlanId), - InstanceName: utils.Ptr(""), + InstanceName: "", EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -253,12 +259,11 @@ func TestParseInput(t *testing.T) { { description: "repeated plugin flags", flagValues: fixtureFlagValues(), - pluginValues: []string{"example-plugin-1", "example-plugin-2"}, + pluginValues: []string{string(rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT), string(rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_CONSISTENT_HASH_EXCHANGE)}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Plugin = utils.Ptr( - append(*model.Plugin, "example-plugin-1", "example-plugin-2"), - ) + model.Plugin = + append(model.Plugin, rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT, rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_CONSISTENT_HASH_EXCHANGE) }), }, { @@ -267,85 +272,19 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = + append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.pluginValues { - err := cmd.Flags().Set(pluginFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", pluginFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + syslogFlag: tt.syslogValues, + flagPlugins.Name(): tt.pluginValues, + }, tt.isValid) }) } } @@ -364,13 +303,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), getOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -388,13 +327,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), getOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -423,13 +362,13 @@ func TestBuildRequest(t *testing.T) { }, ), getOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -440,37 +379,39 @@ func TestBuildRequest(t *testing.T) { { description: "required fields only", model: &inputModel{ + InstanceName: testInstanceName, GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, PlanId: utils.Ptr(testPlanId), }, getOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, }, }, - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(rabbitmq.CreateInstancePayload{PlanId: utils.Ptr(testPlanId), Parameters: &rabbitmq.InstanceParameters{}}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion). + CreateInstancePayload(rabbitmq.CreateInstancePayload{InstanceName: testInstanceName, PlanId: testPlanId, Parameters: &rabbitmq.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &rabbitMQClientMocked{ + settings := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.getOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(settings)) if err != nil { if !tt.isValid { return @@ -480,7 +421,10 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -521,11 +465,10 @@ func Test_outputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, &tt.args.model, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, &tt.args.model, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/instance/delete/delete.go b/internal/cmd/rabbitmq/instance/delete/delete.go index 852fa0d2e..06d705609 100644 --- a/internal/cmd/rabbitmq/instance/delete/delete.go +++ b/internal/cmd/rabbitmq/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a RabbitMQ instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for RabbitMQ instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } diff --git a/internal/cmd/rabbitmq/instance/delete/delete_test.go b/internal/cmd/rabbitmq/instance/delete/delete_test.go index 8064098ad..0670ebd87 100644 --- a/internal/cmd/rabbitmq/instance/delete/delete_test.go +++ b/internal/cmd/rabbitmq/instance/delete/delete_test.go @@ -4,25 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +const testRegion = "eu01" + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testInstanceId, @@ -35,7 +34,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -58,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiDeleteInstanceRequest)) rabbitmq.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -102,7 +103,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/rabbitmq/instance/describe/describe.go b/internal/cmd/rabbitmq/instance/describe/describe.go index 734bd97ce..19dd2b928 100644 --- a/internal/cmd/rabbitmq/instance/describe/describe.go +++ b/internal/cmd/rabbitmq/instance/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a RabbitMQ instance", @@ -83,20 +82,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } @@ -104,39 +95,21 @@ func outputResult(p *print.Printer, outputFormat string, instance *rabbitmq.Inst if instance == nil { return fmt.Errorf("no instance passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ instance: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(instance.InstanceId)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - if lastOperation := instance.LastOperation; lastOperation != nil { - table.AddRow("LAST OPERATION TYPE", utils.PtrString(lastOperation.Type)) - table.AddSeparator() - table.AddRow("LAST OPERATION STATE", utils.PtrString(lastOperation.State)) - table.AddSeparator() - } - table.AddRow("PLAN ID", utils.PtrString(instance.PlanId)) + table.AddRow("LAST OPERATION TYPE", instance.LastOperation.Type) + table.AddSeparator() + table.AddRow("LAST OPERATION STATE", instance.LastOperation.State) + table.AddSeparator() + table.AddRow("PLAN ID", instance.PlanId) // Only show ACL if it's present and not empty if parameters := instance.Parameters; parameters != nil { - acl := (*instance.Parameters)[aclParameterKey] + acl := instance.Parameters[aclParameterKey] aclStr, ok := acl.(string) if ok { if aclStr != "" { @@ -151,5 +124,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *rabbitmq.Inst } return nil - } + }) } diff --git a/internal/cmd/rabbitmq/instance/describe/describe_test.go b/internal/cmd/rabbitmq/instance/describe/describe_test.go index 8fe1f8288..76f4f5023 100644 --- a/internal/cmd/rabbitmq/instance/describe/describe_test.go +++ b/internal/cmd/rabbitmq/instance/describe/describe_test.go @@ -7,19 +7,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" var testInstanceId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -34,7 +34,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -46,6 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, @@ -57,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiGetInstanceRequest)) rabbitmq.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -101,7 +103,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -109,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -117,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,54 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -208,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -242,7 +197,7 @@ func Test_outputResult(t *testing.T) { args: args{ outputFormat: "", instance: &rabbitmq.Instance{ - Parameters: &map[string]interface{}{ + Parameters: map[string]any{ "foo": nil, }, }, @@ -250,11 +205,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/instance/instance.go b/internal/cmd/rabbitmq/instance/instance.go index 2ad311846..fcbc2b7d9 100644 --- a/internal/cmd/rabbitmq/instance/instance.go +++ b/internal/cmd/rabbitmq/instance/instance.go @@ -1,19 +1,19 @@ package instance import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/instance/create" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/instance/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/instance/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for RabbitMQ instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/rabbitmq/instance/list/list.go b/internal/cmd/rabbitmq/instance/list/list.go index 4556f201f..c2b75ecca 100644 --- a/internal/cmd/rabbitmq/instance/list/list.go +++ b/internal/cmd/rabbitmq/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all RabbitMQ instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 RabbitMQ instances`, "$ stackit rabbitmq instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,15 +66,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get RabbitMQ instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil + instances := resp.GetInstances() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -82,7 +79,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +91,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,42 +110,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, instances []rabbitmq.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []rabbitmq.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "LAST OPERATION TYPE", "LAST OPERATION STATE") for i := range instances { @@ -156,15 +133,11 @@ func outputResult(p *print.Printer, outputFormat string, instances []rabbitmq.In var ( opType, opState string ) - if lastOperation := instance.LastOperation; lastOperation != nil { - opType = utils.PtrString(lastOperation.Type) - opState = utils.PtrString(lastOperation.State) - } else { - opType, opState = "n/a", "n/a" - } + opType = string(instance.LastOperation.Type) + opState = string(instance.LastOperation.State) table.AddRow( utils.PtrString(instance.InstanceId), - utils.PtrString(instance.Name), + instance.Name, opType, opState, ) @@ -175,5 +148,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []rabbitmq.In } return nil - } + }) } diff --git a/internal/cmd/rabbitmq/instance/list/list_test.go b/internal/cmd/rabbitmq/instance/list/list_test.go index bbfefdfff..d123d6c88 100644 --- a/internal/cmd/rabbitmq/instance/list/list_test.go +++ b/internal/cmd/rabbitmq/instance/list/list_test.go @@ -7,26 +7,26 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -38,6 +38,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Limit: utils.Ptr(int64(10)), @@ -49,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiListInstancesRequest)) rabbitmq.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +60,7 @@ func fixtureRequest(mods ...func(request *rabbitmq.ApiListInstancesRequest)) rab func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -77,21 +79,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -113,48 +115,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -178,7 +139,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -190,6 +151,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string instances []rabbitmq.Instance } tests := []struct { @@ -213,11 +175,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/instance/update/update.go b/internal/cmd/rabbitmq/instance/update/update.go index b86b9f969..60582d027 100644 --- a/internal/cmd/rabbitmq/instance/update/update.go +++ b/internal/cmd/rabbitmq/instance/update/update.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api/wait" ) const ( @@ -32,7 +33,6 @@ const ( metricsFrequencyFlag = "metrics-frequency" metricsPrefixFlag = "metrics-prefix" monitoringInstanceIdFlag = "monitoring-instance-id" - pluginFlag = "plugin" sgwAclFlag = "acl" syslogFlag = "syslog" planIdFlag = "plan-id" @@ -40,6 +40,12 @@ const ( versionFlag = "version" ) +var flagPlugins = flags.StringEnumSliceFlag( + "plugin", + rabbitmq.AllowedInstanceParametersPluginsInnerEnumValues, + "Plugins", +) + type inputModel struct { *globalflags.GlobalFlagModel InstanceId string @@ -48,16 +54,16 @@ type inputModel struct { EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string - Plugin *[]string + Plugin []rabbitmq.InstanceParametersPluginsInner SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a RabbitMQ instance", @@ -84,22 +90,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := rabbitmqUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -115,13 +119,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for RabbitMQ instance update: %w", err) } - s.Stop() } operationState := "Updated" @@ -139,10 +143,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") - cmd.Flags().StringSlice(pluginFlag, []string{}, "Plugin") + flagPlugins.Register(cmd) cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") cmd.Flags().StringSlice(syslogFlag, []string{}, "Syslog") cmd.Flags().Var(flags.UUIDFlag(), planIdFlag, "Plan ID") @@ -161,11 +165,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu enableMonitoring := flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag) monitoringInstanceId := flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag) graphite := flags.FlagToStringPointer(p, cmd, graphiteFlag) - metricsFrequency := flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag) + metricsFrequency := flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag) metricsPrefix := flags.FlagToStringPointer(p, cmd, metricsPrefixFlag) - plugin := flags.FlagToStringSlicePointer(p, cmd, pluginFlag) + plugin := flagPlugins.Get() sgwAcl := flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag) - syslog := flags.FlagToStringSlicePointer(p, cmd, syslogFlag) + syslog := flags.FlagToStringSliceValue(p, cmd, syslogFlag) planId := flags.FlagToStringPointer(p, cmd, planIdFlag) planName := flags.FlagToStringValue(p, cmd, planNameFlag) version := flags.FlagToStringValue(p, cmd, versionFlag) @@ -200,30 +204,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } type rabbitMQClient interface { - PartialUpdateInstance(ctx context.Context, projectId, instanceId string) rabbitmq.ApiPartialUpdateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*rabbitmq.ListOfferingsResponse, error) + PartialUpdateInstance(ctx context.Context, projectId, regionId, instanceId string) rabbitmq.ApiPartialUpdateInstanceRequest + ListOfferings(ctx context.Context, projectId, regionId string) rabbitmq.ApiListOfferingsRequest } func buildRequest(ctx context.Context, model *inputModel, apiClient rabbitMQClient) (rabbitmq.ApiPartialUpdateInstanceRequest, error) { - req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.Region, model.InstanceId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get RabbitMQ offerings: %w", err) } diff --git a/internal/cmd/rabbitmq/instance/update/update_test.go b/internal/cmd/rabbitmq/instance/update/update_test.go index 50642481c..b636409c9 100644 --- a/internal/cmd/rabbitmq/instance/update/update_test.go +++ b/internal/cmd/rabbitmq/instance/update/update_test.go @@ -5,43 +5,41 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} -type rabbitMQClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *rabbitmq.ListOfferingsResponse } -func (c *rabbitMQClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId string) rabbitmq.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId) -} - -func (c *rabbitMQClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*rabbitmq.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(settings mockSettings) rabbitmq.DefaultAPI { + return rabbitmq.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ rabbitmq.ApiListOfferingsRequest) (*rabbitmq.ListOfferingsResponse, error) { + if settings.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return settings.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var ( testProjectId = uuid.NewString() testInstanceId = uuid.NewString() + testRegion = "eu01" testPlanId = uuid.NewString() testMonitoringInstanceId = uuid.NewString() ) @@ -58,16 +56,17 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - enableMonitoringFlag: "true", - graphiteFlag: "example-graphite", - metricsFrequencyFlag: "100", - metricsPrefixFlag: "example-prefix", - monitoringInstanceIdFlag: testMonitoringInstanceId, - pluginFlag: "example-plugin", - sgwAclFlag: "198.51.100.14/24", - syslogFlag: "example-syslog", - planIdFlag: testPlanId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + enableMonitoringFlag: "true", + graphiteFlag: "example-graphite", + metricsFrequencyFlag: "100", + metricsPrefixFlag: "example-prefix", + monitoringInstanceIdFlag: testMonitoringInstanceId, + flagPlugins.Name(): string(rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT), + sgwAclFlag: "198.51.100.14/24", + syslogFlag: "example-syslog", + planIdFlag: testPlanId, } for _, mod := range mods { mod(flagValues) @@ -79,17 +78,18 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugin: utils.Ptr([]string{"example-plugin"}), + Plugin: []rabbitmq.InstanceParametersPluginsInner{rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT}, SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -99,17 +99,17 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiPartialUpdateInstanceRequest)) rabbitmq.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId) request = request.PartialUpdateInstancePayload(rabbitmq.PartialUpdateInstancePayload{ Parameters: &rabbitmq.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), - Plugins: utils.Ptr([]string{"example-plugin"}), + Plugins: []rabbitmq.InstanceParametersPluginsInner{rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT}, SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, PlanId: utils.Ptr(testPlanId), }) @@ -159,7 +159,7 @@ func TestParseInput(t *testing.T) { description: "required flags only (no values to update)", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, }, isValid: false, expectedModel: &inputModel{ @@ -174,12 +174,12 @@ func TestParseInput(t *testing.T) { description: "zero values", argValues: fixtureArgValues(), flagValues: map[string]string{ - projectIdFlag: testProjectId, - planIdFlag: testPlanId, - enableMonitoringFlag: "false", - graphiteFlag: "", - metricsFrequencyFlag: "0", - metricsPrefixFlag: "", + globalflags.ProjectIdFlag: testProjectId, + planIdFlag: testPlanId, + enableMonitoringFlag: "false", + graphiteFlag: "", + metricsFrequencyFlag: "0", + metricsPrefixFlag: "", }, isValid: true, expectedModel: &inputModel{ @@ -191,7 +191,7 @@ func TestParseInput(t *testing.T) { PlanId: utils.Ptr(testPlanId), EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -199,7 +199,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -207,7 +207,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -215,7 +215,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -270,12 +270,11 @@ func TestParseInput(t *testing.T) { description: "repeated plugin flags", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(), - pluginValues: []string{"example-plugin-1", "example-plugin-2"}, + pluginValues: []string{string(rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT), string(rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_CONSISTENT_HASH_EXCHANGE)}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Plugin = utils.Ptr( - append(*model.Plugin, "example-plugin-1", "example-plugin-2"), - ) + model.Plugin = + append(model.Plugin, rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_MQTT, rabbitmq.INSTANCEPARAMETERSPLUGINSINNER_RABBITMQ_CONSISTENT_HASH_EXCHANGE) }), }, { @@ -285,93 +284,19 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = + append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.pluginValues { - err := cmd.Flags().Set(pluginFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", pluginFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + syslogFlag: tt.syslogValues, + flagPlugins.Name(): tt.pluginValues, + }, tt.isValid) }) } } @@ -390,13 +315,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), listOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -414,13 +339,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), listOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -449,13 +374,13 @@ func TestBuildRequest(t *testing.T) { }, ), listOfferingsResp: &rabbitmq.ListOfferingsResponse{ - Offerings: &[]rabbitmq.Offering{ + Offerings: []rabbitmq.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]rabbitmq.Plan{ + Version: "example-version", + Plans: []rabbitmq.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -468,22 +393,23 @@ func TestBuildRequest(t *testing.T) { model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(rabbitmq.PartialUpdateInstancePayload{Parameters: &rabbitmq.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &rabbitMQClientMocked{ + settings := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.listOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(settings)) if err != nil { if !tt.isValid { return @@ -493,7 +419,10 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/rabbitmq/plans/plans.go b/internal/cmd/rabbitmq/plans/plans.go index 996cea081..13f18dabf 100644 --- a/internal/cmd/rabbitmq/plans/plans.go +++ b/internal/cmd/rabbitmq/plans/plans.go @@ -2,11 +2,10 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,10 +15,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) const ( @@ -31,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists all RabbitMQ service plans", @@ -48,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 RabbitMQ service plans`, "$ stackit rabbitmq plans --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,15 +65,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get RabbitMQ service plans: %w", err) } - plans := *resp.Offerings - if len(plans) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No plans found for project %q\n", projectLabel) - return nil + plans := resp.GetOfferings() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output @@ -83,7 +78,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { plans = plans[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, plans) + return outputResult(params.Printer, model.OutputFormat, projectLabel, plans) }, } @@ -95,7 +90,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -114,55 +109,35 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.APIClient) rabbitmq.ApiListOfferingsRequest { - req := apiClient.ListOfferings(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListOfferings(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, plans []rabbitmq.Offering) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("marshal RabbitMQ plans: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, plans []rabbitmq.Offering) error { + return p.OutputResult(outputFormat, plans, func() error { + if len(plans) == 0 { + p.Outputf("No plans found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal RabbitMQ plans: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] if o.Plans != nil { - for j := range *o.Plans { - plan := (*o.Plans)[j] + for j := range o.Plans { + plan := o.Plans[j] table.AddRow( - utils.PtrString(o.Name), - utils.PtrString(o.Version), - utils.PtrString(plan.Id), - utils.PtrString(plan.Name), - utils.PtrString(plan.Description), + o.Name, + o.Version, + plan.Id, + plan.Name, + plan.Description, ) } table.AddSeparator() @@ -175,5 +150,5 @@ func outputResult(p *print.Printer, outputFormat string, plans []rabbitmq.Offeri } return nil - } + }) } diff --git a/internal/cmd/rabbitmq/plans/plans_test.go b/internal/cmd/rabbitmq/plans/plans_test.go index d5f0f9aee..0eaa9f024 100644 --- a/internal/cmd/rabbitmq/plans/plans_test.go +++ b/internal/cmd/rabbitmq/plans/plans_test.go @@ -7,26 +7,26 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) -var projectIdFlag = globalflags.ProjectIdFlag - type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &rabbitmq.APIClient{} +var testClient = &rabbitmq.APIClient{DefaultAPI: &rabbitmq.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -38,6 +38,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Limit: utils.Ptr(int64(10)), @@ -49,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *rabbitmq.ApiListOfferingsRequest)) rabbitmq.ApiListOfferingsRequest { - request := testClient.ListOfferings(testCtx, testProjectId) + request := testClient.DefaultAPI.ListOfferings(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +60,7 @@ func fixtureRequest(mods ...func(request *rabbitmq.ApiListOfferingsRequest)) rab func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -77,21 +79,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -113,48 +115,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -178,7 +139,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, rabbitmq.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -190,6 +151,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string plans []rabbitmq.Offering } tests := []struct { @@ -213,11 +175,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.plans); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/rabbitmq/rabbitmq.go b/internal/cmd/rabbitmq/rabbitmq.go index 9dc5b76a3..23099b758 100644 --- a/internal/cmd/rabbitmq/rabbitmq.go +++ b/internal/cmd/rabbitmq/rabbitmq.go @@ -1,17 +1,17 @@ package rabbitmq import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/credentials" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/rabbitmq/plans" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "rabbitmq", Short: "Provides functionality for RabbitMQ", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(plans.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) diff --git a/internal/cmd/redis/credentials/create/create.go b/internal/cmd/redis/credentials/create/create.go index 3e6d2d961..99738fd25 100644 --- a/internal/cmd/redis/credentials/create/create.go +++ b/internal/cmd/redis/credentials/create/create.go @@ -2,11 +2,10 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { ShowPassword bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates credentials for a Redis instance", @@ -46,9 +45,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create credentials for a Redis instance and show the password in the output`, "$ stackit redis credentials create --instance-id xxx --show-password"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -59,18 +58,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create credentials for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -95,7 +92,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -107,20 +104,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ShowPassword: flags.FlagToBoolValue(p, cmd, showPasswordFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiCreateCredentialsRequest { - req := apiClient.CreateCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.CreateCredentials(ctx, model.ProjectId, model.Region, model.InstanceId) return req } @@ -133,47 +122,27 @@ func outputResult(p *print.Printer, model inputModel, instanceLabel string, resp } if !model.ShowPassword { if resp.Raw == nil { - resp.Raw = &redis.RawCredentials{} - } - if resp.Raw.Credentials == nil { - resp.Raw.Credentials = &redis.Credentials{} + resp.Raw = &redis.RawCredentials{Credentials: redis.Credentials{}} } - resp.Raw.Credentials.Password = utils.Ptr("hidden") + resp.Raw.Credentials.Password = "hidden" } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, utils.PtrString(resp.Id)) + return p.OutputResult(model.OutputFormat, resp, func() error { + p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, resp.Id) // The username field cannot be set by the user, so we only display it if it's not returned empty - if resp.HasRaw() && resp.Raw.Credentials != nil { - if username := resp.Raw.Credentials.Username; username != nil && *username != "" { - p.Outputf("Username: %s\n", utils.PtrString(username)) + if resp.HasRaw() { + if username := resp.Raw.Credentials.Username; username != "" { + p.Outputf("Username: %s\n", username) } if !model.ShowPassword { p.Outputf("Password: \n") } else { - p.Outputf("Password: %s\n", utils.PtrString(resp.Raw.Credentials.Password)) + p.Outputf("Password: %s\n", resp.Raw.Credentials.Password) } - p.Outputf("Host: %s\n", utils.PtrString(resp.Raw.Credentials.Host)) + p.Outputf("Host: %s\n", resp.Raw.Credentials.Host) p.Outputf("Port: %s\n", utils.PtrString(resp.Raw.Credentials.Port)) } - p.Outputf("URI: %s\n", utils.PtrString(resp.Uri)) + p.Outputf("URI: %s\n", resp.Uri) return nil - } + }) } diff --git a/internal/cmd/redis/credentials/create/create_test.go b/internal/cmd/redis/credentials/create/create_test.go index 070229478..cf37c32b3 100644 --- a/internal/cmd/redis/credentials/create/create_test.go +++ b/internal/cmd/redis/credentials/create/create_test.go @@ -7,10 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -18,14 +19,16 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -38,6 +41,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, } @@ -48,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiCreateCredentialsRequest)) redis.ApiCreateCredentialsRequest { - request := testClient.CreateCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.CreateCredentials(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -58,6 +62,7 @@ func fixtureRequest(mods ...func(request *redis.ApiCreateCredentialsRequest)) re func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -129,46 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -191,7 +157,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -223,11 +189,10 @@ func Test_outputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.instanceLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/credentials/credentials.go b/internal/cmd/redis/credentials/credentials.go index d1d8a4d7a..41a7b4f92 100644 --- a/internal/cmd/redis/credentials/credentials.go +++ b/internal/cmd/redis/credentials/credentials.go @@ -1,18 +1,18 @@ package credentials import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/credentials/create" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/credentials/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for Redis credentials", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/redis/credentials/delete/delete.go b/internal/cmd/redis/credentials/delete/delete.go index 4d438bfba..90bcb67b4 100644 --- a/internal/cmd/redis/credentials/delete/delete.go +++ b/internal/cmd/redis/credentials/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", credentialsIdArg), Short: "Deletes credentials of a Redis instance", @@ -55,24 +56,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - credentialsLabel, err := redisUtils.GetCredentialsUsername(ctx, apiClient, model.ProjectId, model.InstanceId, model.CredentialsId) + credentialsLabel, err := redisUtils.GetCredentialsUsername(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.CredentialsId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get credentials user name: %v", err) credentialsLabel = model.CredentialsId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete credentials %s of instance %q? (This cannot be undone)", credentialsLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,19 +110,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiDeleteCredentialsRequest { - req := apiClient.DeleteCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.DeleteCredentials(ctx, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) return req } diff --git a/internal/cmd/redis/credentials/delete/delete_test.go b/internal/cmd/redis/credentials/delete/delete_test.go index d8b2c4632..c6bf42dd6 100644 --- a/internal/cmd/redis/credentials/delete/delete_test.go +++ b/internal/cmd/redis/credentials/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,10 +18,11 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() +var testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -36,8 +36,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -50,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, CredentialsId: testCredentialsId, @@ -61,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiDeleteCredentialsRequest)) redis.ApiDeleteCredentialsRequest { - request := testClient.DeleteCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.DeleteCredentials(testCtx, testProjectId, testRegion, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -165,54 +167,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -235,7 +190,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/redis/credentials/describe/describe.go b/internal/cmd/redis/credentials/describe/describe.go index 028554868..359e0f86e 100644 --- a/internal/cmd/redis/credentials/describe/describe.go +++ b/internal/cmd/redis/credentials/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { CredentialsId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", credentialsIdArg), Short: "Shows details of credentials of a Redis instance", @@ -95,20 +94,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CredentialsId: credentialsId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiGetCredentialsRequest { - req := apiClient.GetCredentials(ctx, model.ProjectId, model.InstanceId, model.CredentialsId) + req := apiClient.DefaultAPI.GetCredentials(ctx, model.ProjectId, model.Region, model.InstanceId, model.CredentialsId) return req } @@ -116,34 +107,18 @@ func outputResult(p *print.Printer, outputFormat string, credentials *redis.Cred if credentials == nil { return fmt.Errorf("no credentials passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis credentials: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis credentials: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, credentials, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(credentials.Id)) + table.AddRow("ID", credentials.Id) table.AddSeparator() // The username field cannot be set by the user so we only display it if it's not returned empty - if credentials.HasRaw() && credentials.Raw.Credentials != nil { - if username := credentials.Raw.Credentials.Username; username != nil && *username != "" { - table.AddRow("USERNAME", *username) + if credentials.HasRaw() { + if username := credentials.Raw.Credentials.Username; username != "" { + table.AddRow("USERNAME", username) table.AddSeparator() } - table.AddRow("PASSWORD", utils.PtrString(credentials.Raw.Credentials.Password)) + table.AddRow("PASSWORD", credentials.Raw.Credentials.Password) table.AddSeparator() table.AddRow("URI", utils.PtrString(credentials.Raw.Credentials.Uri)) } @@ -153,5 +128,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials *redis.Cred } return nil - } + }) } diff --git a/internal/cmd/redis/credentials/describe/describe_test.go b/internal/cmd/redis/credentials/describe/describe_test.go index fc8edb1b8..60474623f 100644 --- a/internal/cmd/redis/credentials/describe/describe_test.go +++ b/internal/cmd/redis/credentials/describe/describe_test.go @@ -7,10 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -18,10 +19,11 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testCredentialsId = uuid.NewString() +var testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -35,8 +37,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -49,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, CredentialsId: testCredentialsId, @@ -60,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiGetCredentialsRequest)) redis.ApiGetCredentialsRequest { - request := testClient.GetCredentials(testCtx, testProjectId, testInstanceId, testCredentialsId) + request := testClient.DefaultAPI.GetCredentials(testCtx, testProjectId, testRegion, testInstanceId, testCredentialsId) for _, mod := range mods { mod(&request) } @@ -164,54 +168,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -234,7 +191,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -267,11 +224,10 @@ func Test_outputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/credentials/list/list.go b/internal/cmd/redis/credentials/list/list.go index d7edc501e..948e57522 100644 --- a/internal/cmd/redis/credentials/list/list.go +++ b/internal/cmd/redis/credentials/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/redis/client" redisUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/redis/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all credentials' IDs for a Redis instance", @@ -49,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 credentials' IDs for a Redis instance`, "$ stackit redis credentials list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,22 +67,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list Redis credentials: %w", err) } - credentials := *resp.CredentialsList + credentials := resp.CredentialsList + + // Truncate output + if model.Limit != nil && len(credentials) > int(*model.Limit) { + credentials = credentials[:*model.Limit] + } + + instanceLabel := model.InstanceId if len(credentials) == 0 { - instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err = redisUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId } - params.Printer.Info("No credentials found for instance %q\n", instanceLabel) - return nil } - // Truncate output - if model.Limit != nil && len(credentials) > int(*model.Limit) { - credentials = credentials[:*model.Limit] - } - return outputResult(params.Printer, model.OutputFormat, credentials) + return outputResult(params.Printer, model.OutputFormat, instanceLabel, credentials) }, } configureFlags(cmd) @@ -98,7 +97,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -118,47 +117,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiListCredentialsRequest { - req := apiClient.ListCredentials(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.ListCredentials(ctx, model.ProjectId, model.Region, model.InstanceId) return req } -func outputResult(p *print.Printer, outputFormat string, credentials []redis.CredentialsListItem) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(credentials, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis credentials list: %w", err) +func outputResult(p *print.Printer, outputFormat, instanceLabel string, credentials []redis.CredentialsListItem) error { + return p.OutputResult(outputFormat, credentials, func() error { + if len(credentials) == 0 { + p.Outputf("No credentials found for instance %q\n", instanceLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis credentials list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID") for i := range credentials { c := credentials[i] - table.AddRow(utils.PtrString(c.Id)) + table.AddRow(c.Id) } err := table.Display(p) if err != nil { @@ -166,5 +145,5 @@ func outputResult(p *print.Printer, outputFormat string, credentials []redis.Cre } return nil - } + }) } diff --git a/internal/cmd/redis/credentials/list/list_test.go b/internal/cmd/redis/credentials/list/list_test.go index a37f530c4..4ac005e63 100644 --- a/internal/cmd/redis/credentials/list/list_test.go +++ b/internal/cmd/redis/credentials/list/list_test.go @@ -7,11 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,15 +20,17 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - instanceIdFlag: testInstanceId, - limitFlag: "10", + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -40,6 +43,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, Limit: utils.Ptr(int64(10)), @@ -51,7 +55,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiListCredentialsRequest)) redis.ApiListCredentialsRequest { - request := testClient.ListCredentials(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListCredentials(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -61,6 +65,7 @@ func fixtureRequest(mods ...func(request *redis.ApiListCredentialsRequest)) redi func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -136,46 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -198,7 +164,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -234,11 +200,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.credentials); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, "dummy-instance-label", tt.args.credentials); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/instance/create/create.go b/internal/cmd/redis/instance/create/create.go index bece1146e..ebcf4a828 100644 --- a/internal/cmd/redis/instance/create/create.go +++ b/internal/cmd/redis/instance/create/create.go @@ -2,13 +2,12 @@ package create import ( "context" - "encoding/json" "errors" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -22,8 +21,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api/wait" ) const ( @@ -45,18 +44,18 @@ type inputModel struct { PlanName string Version string - InstanceName *string + InstanceName string EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Redis instance", @@ -73,9 +72,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a Redis instance with name "my-instance" and specify IP range which is allowed to access it`, "$ stackit redis instance create --name my-instance --plan-id xxx --acl 1.2.3.0/24"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -92,16 +91,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a Redis instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a Redis instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -113,17 +110,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create Redis instance: %w", err) } - instanceId := *resp.InstanceId + instanceId := resp.InstanceId // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating instance") - _, err = wait.CreateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating instance", func() error { + _, err = wait.CreateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Redis instance creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, instanceId, resp) @@ -137,7 +134,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") @@ -150,7 +147,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -173,49 +170,36 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), EnableMonitoring: flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag), MonitoringInstanceId: flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag), Graphite: flags.FlagToStringPointer(p, cmd, graphiteFlag), - MetricsFrequency: flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag), + MetricsFrequency: flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag), MetricsPrefix: flags.FlagToStringPointer(p, cmd, metricsPrefixFlag), SgwAcl: flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag), - Syslog: flags.FlagToStringSlicePointer(p, cmd, syslogFlag), + Syslog: flags.FlagToStringSliceValue(p, cmd, syslogFlag), PlanId: planId, PlanName: planName, Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type redisClient interface { - CreateInstance(ctx context.Context, projectId string) redis.ApiCreateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*redis.ListOfferingsResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient redisClient) (redis.ApiCreateInstanceRequest, error) { - req := apiClient.CreateInstance(ctx, model.ProjectId) +func buildRequest(ctx context.Context, model *inputModel, apiClient redis.DefaultAPI) (redis.ApiCreateInstanceRequest, error) { + req := apiClient.CreateInstance(ctx, model.ProjectId, model.Region) - var planId *string + var planId string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get Redis offerings: %w", err) } if model.PlanId == nil { - planId, err = redisUtils.LoadPlanId(model.PlanName, model.Version, offerings) + foundPlanId, err := redisUtils.LoadPlanId(model.PlanName, model.Version, offerings) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -223,12 +207,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient redisClient) } return req, err } + planId = *foundPlanId } else { err := redisUtils.ValidatePlanId(*model.PlanId, offerings) if err != nil { return req, err } - planId = model.PlanId + planId = *model.PlanId } var sgwAcl *string @@ -262,29 +247,13 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId if resp == nil { return fmt.Errorf("no response defined") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis instance: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, resp, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } p.Outputf("%s instance for project %q. Instance ID: %s\n", operationState, projectLabel, instanceId) return nil - } + }) } diff --git a/internal/cmd/redis/instance/create/create_test.go b/internal/cmd/redis/instance/create/create_test.go index 814673259..a4f1a3348 100644 --- a/internal/cmd/redis/instance/create/create_test.go +++ b/internal/cmd/redis/instance/create/create_test.go @@ -8,11 +8,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,31 +21,32 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testPlanId = uuid.NewString() +var testMonitoringInstanceId = uuid.NewString() +var testRegion = "eu01" -type redisClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *redis.ListOfferingsResponse } -func (c *redisClientMocked) CreateInstance(ctx context.Context, projectId string) redis.ApiCreateInstanceRequest { - return testClient.CreateInstance(ctx, projectId) -} - -func (c *redisClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*redis.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) redis.DefaultAPI { + return &redis.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ redis.ApiListOfferingsRequest) (*redis.ListOfferingsResponse, error) { + if s.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return s.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } -var testProjectId = uuid.NewString() -var testPlanId = uuid.NewString() -var testMonitoringInstanceId = uuid.NewString() - func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ projectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, instanceNameFlag: "example-name", enableMonitoringFlag: "true", graphiteFlag: "example-graphite", @@ -66,15 +68,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -84,19 +87,19 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiCreateInstanceRequest)) redis.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion) request = request.CreateInstancePayload(redis.CreateInstancePayload{ - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", Parameters: &redis.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, - PlanId: utils.Ptr(testPlanId), + PlanId: testPlanId, }) for _, mod := range mods { mod(&request) @@ -107,6 +110,7 @@ func fixtureRequest(mods ...func(request *redis.ApiCreateInstanceRequest)) redis func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string sgwAclValues []string syslogValues []string @@ -151,7 +155,7 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example-name"), + InstanceName: "example-name", PlanId: utils.Ptr(testPlanId), }, }, @@ -173,10 +177,10 @@ func TestParseInput(t *testing.T) { Verbosity: globalflags.VerbosityDefault, }, PlanId: utils.Ptr(testPlanId), - InstanceName: utils.Ptr(""), + InstanceName: "", EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -252,75 +256,17 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.sgwAclValues { - err := cmd.Flags().Set(sgwAclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", sgwAclFlag, value, err) - } - } - - for _, value := range tt.syslogValues { - err := cmd.Flags().Set(syslogFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", syslogFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + sgwAclFlag: tt.sgwAclValues, + syslogFlag: tt.syslogValues, + }, tt.isValid) }) } } @@ -339,13 +285,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), getOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -363,13 +309,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), getOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -398,13 +344,13 @@ func TestBuildRequest(t *testing.T) { }, ), getOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -418,34 +364,35 @@ func TestBuildRequest(t *testing.T) { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, PlanId: utils.Ptr(testPlanId), }, getOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, }, }, - expectedRequest: testClient.CreateInstance(testCtx, testProjectId). - CreateInstancePayload(redis.CreateInstancePayload{PlanId: utils.Ptr(testPlanId), Parameters: &redis.InstanceParameters{}}), + expectedRequest: testClient.DefaultAPI.CreateInstance(testCtx, testProjectId, testRegion). + CreateInstancePayload(redis.CreateInstancePayload{PlanId: testPlanId, Parameters: &redis.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &redisClientMocked{ + client := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.getOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(client)) if err != nil { if !tt.isValid { return @@ -454,8 +401,11 @@ func TestBuildRequest(t *testing.T) { } diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -496,11 +446,10 @@ func Test_outputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.instanceId, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/instance/delete/delete.go b/internal/cmd/redis/instance/delete/delete.go index 01fa78218..df23695e1 100644 --- a/internal/cmd/redis/instance/delete/delete.go +++ b/internal/cmd/redis/instance/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api/wait" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a Redis instance", @@ -53,18 +54,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -76,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting instance") - _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting instance", func() error { + _, err = wait.DeleteInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.InstanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Redis instance deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } diff --git a/internal/cmd/redis/instance/delete/delete_test.go b/internal/cmd/redis/instance/delete/delete_test.go index bb0da281f..68617b67c 100644 --- a/internal/cmd/redis/instance/delete/delete_test.go +++ b/internal/cmd/redis/instance/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,9 +18,10 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +var testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -35,7 +35,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + projectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, } @@ -58,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiDeleteInstanceRequest)) redis.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +140,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -208,7 +163,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { diff --git a/internal/cmd/redis/instance/describe/describe.go b/internal/cmd/redis/instance/describe/describe.go index 22e9e4805..6b5f395d7 100644 --- a/internal/cmd/redis/instance/describe/describe.go +++ b/internal/cmd/redis/instance/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a Redis instance", @@ -83,20 +82,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.Region, model.InstanceId) return req } @@ -104,39 +95,21 @@ func outputResult(p *print.Printer, outputFormat string, instance *redis.Instanc if instance == nil { return fmt.Errorf("no instance passed") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis instance: %w", err) - } - p.Outputln(string(details)) - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(instance.InstanceId)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - if lastOperation := instance.LastOperation; lastOperation != nil { - table.AddRow("LAST OPERATION TYPE", utils.PtrString(instance.LastOperation.Type)) - table.AddSeparator() - table.AddRow("LAST OPERATION STATE", utils.PtrString(instance.LastOperation.State)) - table.AddSeparator() - } - table.AddRow("PLAN ID", utils.PtrString(instance.PlanId)) + table.AddRow("LAST OPERATION TYPE", instance.LastOperation.Type) + table.AddSeparator() + table.AddRow("LAST OPERATION STATE", instance.LastOperation.State) + table.AddSeparator() + table.AddRow("PLAN ID", instance.PlanId) if parameters := instance.Parameters; parameters != nil { // Only show ACL if it's present and not empty - acl := (*parameters)[aclParameterKey] + acl := parameters[aclParameterKey] aclStr, ok := acl.(string) if ok { if aclStr != "" { @@ -151,5 +124,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *redis.Instanc } return nil - } + }) } diff --git a/internal/cmd/redis/instance/describe/describe_test.go b/internal/cmd/redis/instance/describe/describe_test.go index 651164b98..24521eae9 100644 --- a/internal/cmd/redis/instance/describe/describe_test.go +++ b/internal/cmd/redis/instance/describe/describe_test.go @@ -7,10 +7,11 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -18,9 +19,10 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +var testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ @@ -34,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + projectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, } @@ -57,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiGetInstanceRequest)) redis.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testRegion, testInstanceId) for _, mod := range mods { mod(&request) } @@ -137,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -207,7 +164,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -243,7 +200,7 @@ func Test_outputResult(t *testing.T) { name: "nil parameter", args: args{ instance: &redis.Instance{ - Parameters: &map[string]interface{}{ + Parameters: map[string]interface{}{ "foo": nil, }, }, @@ -251,11 +208,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/instance/instance.go b/internal/cmd/redis/instance/instance.go index a45e6bd96..82cbe63cd 100644 --- a/internal/cmd/redis/instance/instance.go +++ b/internal/cmd/redis/instance/instance.go @@ -1,19 +1,19 @@ package instance import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/instance/create" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/instance/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/instance/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for Redis instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/redis/instance/list/list.go b/internal/cmd/redis/instance/list/list.go index cc2bcacbd..9d9de28b7 100644 --- a/internal/cmd/redis/instance/list/list.go +++ b/internal/cmd/redis/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/redis/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Redis instances", @@ -47,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Redis instances`, "$ stackit redis instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,23 +66,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get Redis instances: %w", err) } - instances := *resp.Instances - if len(instances) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No instances found for project %q\n", projectLabel) - return nil - } + instances := resp.Instances // Truncate output if model.Limit != nil && len(instances) > int(*model.Limit) { instances = instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + projectLabel := model.ProjectId + if len(instances) == 0 { + projectLabel, err = projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, instances) }, } @@ -94,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,42 +112,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, instances []redis.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, instances []redis.Instance) error { + return p.OutputResult(outputFormat, instances, func() error { + if len(instances) == 0 { + p.Outputf("No instances found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "LAST OPERATION TYPE", "LAST OPERATION STATE") for i := range instances { @@ -156,12 +135,11 @@ func outputResult(p *print.Printer, outputFormat string, instances []redis.Insta var ( lastOperationType, lastOperationState string ) - if lastOperation := instance.LastOperation; lastOperation != nil { - lastOperationType, lastOperationState = utils.PtrString(lastOperation.Type), utils.PtrString(lastOperation.State) - } + lastOperationType = string(instance.LastOperation.Type) + lastOperationState = string(instance.LastOperation.State) table.AddRow( utils.PtrString(instance.InstanceId), - utils.PtrString(instance.Name), + instance.Name, lastOperationType, lastOperationState, ) @@ -172,5 +150,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []redis.Insta } return nil - } + }) } diff --git a/internal/cmd/redis/instance/list/list_test.go b/internal/cmd/redis/instance/list/list_test.go index aff5c8020..cc30a0ca2 100644 --- a/internal/cmd/redis/instance/list/list_test.go +++ b/internal/cmd/redis/instance/list/list_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,13 +20,15 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + projectIdFlag: testProjectId, + limitFlag: "10", + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -39,6 +41,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), } @@ -49,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiListInstancesRequest)) redis.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +62,7 @@ func fixtureRequest(mods ...func(request *redis.ApiListInstancesRequest)) redis. func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -113,48 +117,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -177,7 +140,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -212,11 +175,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, "dummy-project-label", tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/instance/update/update.go b/internal/cmd/redis/instance/update/update.go index 6d2e919f7..170089ad1 100644 --- a/internal/cmd/redis/instance/update/update.go +++ b/internal/cmd/redis/instance/update/update.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/redis" - "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api/wait" ) const ( @@ -46,15 +47,15 @@ type inputModel struct { EnableMonitoring *bool Graphite *string - MetricsFrequency *int64 + MetricsFrequency *int32 MetricsPrefix *string MonitoringInstanceId *string SgwAcl *[]string - Syslog *[]string + Syslog []string PlanId *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a Redis instance", @@ -81,22 +82,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := redisUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.Region) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API - req, err := buildRequest(ctx, model, apiClient) + req, err := buildRequest(ctx, model, apiClient.DefaultAPI) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -112,13 +111,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating instance") - _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient, model.ProjectId, instanceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating instance", func() error { + _, err = wait.PartialUpdateInstanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, instanceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for Redis instance update: %w", err) } - s.Stop() } operationState := "Updated" @@ -136,7 +135,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(enableMonitoringFlag, false, "Enable monitoring") cmd.Flags().String(graphiteFlag, "", "Graphite host") - cmd.Flags().Int64(metricsFrequencyFlag, 0, "Metrics frequency") + cmd.Flags().Int32(metricsFrequencyFlag, 0, "Metrics frequency") cmd.Flags().String(metricsPrefixFlag, "", "Metrics prefix") cmd.Flags().Var(flags.UUIDFlag(), monitoringInstanceIdFlag, "Monitoring instance ID") cmd.Flags().Var(flags.CIDRSliceFlag(), sgwAclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") @@ -157,10 +156,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu enableMonitoring := flags.FlagToBoolPointer(p, cmd, enableMonitoringFlag) monitoringInstanceId := flags.FlagToStringPointer(p, cmd, monitoringInstanceIdFlag) graphite := flags.FlagToStringPointer(p, cmd, graphiteFlag) - metricsFrequency := flags.FlagToInt64Pointer(p, cmd, metricsFrequencyFlag) + metricsFrequency := flags.FlagToInt32Pointer(p, cmd, metricsFrequencyFlag) metricsPrefix := flags.FlagToStringPointer(p, cmd, metricsPrefixFlag) sgwAcl := flags.FlagToStringSlicePointer(p, cmd, sgwAclFlag) - syslog := flags.FlagToStringSlicePointer(p, cmd, syslogFlag) + syslog := flags.FlagToStringSliceValue(p, cmd, syslogFlag) planId := flags.FlagToStringPointer(p, cmd, planIdFlag) planName := flags.FlagToStringValue(p, cmd, planNameFlag) version := flags.FlagToStringValue(p, cmd, versionFlag) @@ -194,30 +193,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Version: version, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -type redisClient interface { - PartialUpdateInstance(ctx context.Context, projectId, instanceId string) redis.ApiPartialUpdateInstanceRequest - ListOfferingsExecute(ctx context.Context, projectId string) (*redis.ListOfferingsResponse, error) -} - -func buildRequest(ctx context.Context, model *inputModel, apiClient redisClient) (redis.ApiPartialUpdateInstanceRequest, error) { - req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.InstanceId) +func buildRequest(ctx context.Context, model *inputModel, apiClient redis.DefaultAPI) (redis.ApiPartialUpdateInstanceRequest, error) { + req := apiClient.PartialUpdateInstance(ctx, model.ProjectId, model.Region, model.InstanceId) var planId *string var err error - offerings, err := apiClient.ListOfferingsExecute(ctx, model.ProjectId) + offerings, err := apiClient.ListOfferings(ctx, model.ProjectId, model.Region).Execute() if err != nil { return req, fmt.Errorf("get Redis offerings: %w", err) } diff --git a/internal/cmd/redis/instance/update/update_test.go b/internal/cmd/redis/instance/update/update_test.go index 7c5df24d5..46ca37dc8 100644 --- a/internal/cmd/redis/instance/update/update_test.go +++ b/internal/cmd/redis/instance/update/update_test.go @@ -5,15 +5,14 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,22 +20,23 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} +var testRegion = "eu01" -type redisClientMocked struct { +type mockSettings struct { returnError bool listOfferingsResp *redis.ListOfferingsResponse } -func (c *redisClientMocked) PartialUpdateInstance(ctx context.Context, projectId, instanceId string) redis.ApiPartialUpdateInstanceRequest { - return testClient.PartialUpdateInstance(ctx, projectId, instanceId) -} - -func (c *redisClientMocked) ListOfferingsExecute(_ context.Context, _ string) (*redis.ListOfferingsResponse, error) { - if c.returnError { - return nil, fmt.Errorf("list flavors failed") +func newAPIMock(s mockSettings) redis.DefaultAPI { + return &redis.DefaultAPIServiceMock{ + ListOfferingsExecuteMock: utils.Ptr(func(_ redis.ApiListOfferingsRequest) (*redis.ListOfferingsResponse, error) { + if s.returnError { + return nil, fmt.Errorf("list flavors failed") + } + return s.listOfferingsResp, nil + }), } - return c.listOfferingsResp, nil } var ( @@ -59,6 +59,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ projectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, enableMonitoringFlag: "true", graphiteFlag: "example-graphite", metricsFrequencyFlag: "100", @@ -79,15 +80,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr([]string{"198.51.100.14/24"}), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, PlanId: utils.Ptr(testPlanId), } for _, mod := range mods { @@ -97,16 +99,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiPartialUpdateInstanceRequest)) redis.ApiPartialUpdateInstanceRequest { - request := testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId) request = request.PartialUpdateInstancePayload(redis.PartialUpdateInstancePayload{ Parameters: &redis.InstanceParameters{ EnableMonitoring: utils.Ptr(true), Graphite: utils.Ptr("example-graphite"), - MetricsFrequency: utils.Ptr(int64(100)), + MetricsFrequency: utils.Ptr(int32(100)), MetricsPrefix: utils.Ptr("example-prefix"), MonitoringInstanceId: utils.Ptr(testMonitoringInstanceId), SgwAcl: utils.Ptr("198.51.100.14/24"), - Syslog: utils.Ptr([]string{"example-syslog"}), + Syslog: []string{"example-syslog"}, }, PlanId: utils.Ptr(testPlanId), }) @@ -187,7 +189,7 @@ func TestParseInput(t *testing.T) { PlanId: utils.Ptr(testPlanId), EnableMonitoring: utils.Ptr(false), Graphite: utils.Ptr(""), - MetricsFrequency: utils.Ptr(int64(0)), + MetricsFrequency: utils.Ptr(int32(0)), MetricsPrefix: utils.Ptr(""), }, }, @@ -269,17 +271,15 @@ func TestParseInput(t *testing.T) { syslogValues: []string{"example-syslog-1", "example-syslog-2"}, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Syslog = utils.Ptr( - append(*model.Syslog, "example-syslog-1", "example-syslog-2"), - ) + model.Syslog = append(model.Syslog, "example-syslog-1", "example-syslog-2") }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -331,7 +331,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -364,13 +364,13 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), listOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -388,13 +388,13 @@ func TestBuildRequest(t *testing.T) { ), expectedRequest: fixtureRequest(), listOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("example-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "example-plan-name", + Id: testPlanId, }, }, }, @@ -423,13 +423,13 @@ func TestBuildRequest(t *testing.T) { }, ), listOfferingsResp: &redis.ListOfferingsResponse{ - Offerings: &[]redis.Offering{ + Offerings: []redis.Offering{ { - Version: utils.Ptr("example-version"), - Plans: &[]redis.Plan{ + Version: "example-version", + Plans: []redis.Plan{ { - Name: utils.Ptr("other-plan-name"), - Id: utils.Ptr(testPlanId), + Name: "other-plan-name", + Id: testPlanId, }, }, }, @@ -443,21 +443,22 @@ func TestBuildRequest(t *testing.T) { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, InstanceId: testInstanceId, }, - expectedRequest: testClient.PartialUpdateInstance(testCtx, testProjectId, testInstanceId). + expectedRequest: testClient.DefaultAPI.PartialUpdateInstance(testCtx, testProjectId, testRegion, testInstanceId). PartialUpdateInstancePayload(redis.PartialUpdateInstancePayload{Parameters: &redis.InstanceParameters{}}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &redisClientMocked{ + client := mockSettings{ returnError: tt.getOfferingsFails, listOfferingsResp: tt.listOfferingsResp, } - request, err := buildRequest(testCtx, tt.model, client) + request, err := buildRequest(testCtx, tt.model, newAPIMock(client)) if err != nil { if !tt.isValid { return @@ -466,8 +467,11 @@ func TestBuildRequest(t *testing.T) { } diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), + cmp.FilterPath(func(p cmp.Path) bool { + return p.String() == "ApiService" + }, cmp.Ignore()), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/redis/plans/plans.go b/internal/cmd/redis/plans/plans.go index 7e663c8c3..ede5f2a76 100644 --- a/internal/cmd/redis/plans/plans.go +++ b/internal/cmd/redis/plans/plans.go @@ -2,12 +2,13 @@ package plans import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/redis/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "plans", Short: "Lists all Redis service plans", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Redis service plans`, "$ stackit redis plans --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,7 +65,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get Redis service plans: %w", err) } - plans := *resp.Offerings + plans := resp.Offerings if len(plans) == 0 { projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) if err != nil { @@ -94,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,55 +112,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APIClient) redis.ApiListOfferingsRequest { - req := apiClient.ListOfferings(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListOfferings(ctx, model.ProjectId, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, plans []redis.Offering) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(plans, "", " ") - if err != nil { - return fmt.Errorf("marshal Redis plans: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Redis plans: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, plans, func() error { table := tables.NewTable() table.SetHeader("OFFERING NAME", "VERSION", "ID", "NAME", "DESCRIPTION") for i := range plans { o := plans[i] if o.Plans != nil { - for j := range *o.Plans { - plan := (*o.Plans)[j] + for j := range o.Plans { + plan := (o.Plans)[j] table.AddRow( - utils.PtrString(o.Name), - utils.PtrString(o.Version), - utils.PtrString(plan.Id), - utils.PtrString(plan.Name), - utils.PtrString(plan.Description), + o.Name, + o.Version, + plan.Id, + plan.Name, + plan.Description, ) } table.AddSeparator() @@ -174,5 +148,5 @@ func outputResult(p *print.Printer, outputFormat string, plans []redis.Offering) } return nil - } + }) } diff --git a/internal/cmd/redis/plans/plans_test.go b/internal/cmd/redis/plans/plans_test.go index 8ea65f786..17dae4bd3 100644 --- a/internal/cmd/redis/plans/plans_test.go +++ b/internal/cmd/redis/plans/plans_test.go @@ -7,12 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,13 +20,15 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &redis.APIClient{} +var testClient = &redis.APIClient{DefaultAPI: &redis.DefaultAPIService{}} var testProjectId = uuid.NewString() +var testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + projectIdFlag: testProjectId, + limitFlag: "10", + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -39,6 +41,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), } @@ -49,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *redis.ApiListOfferingsRequest)) redis.ApiListOfferingsRequest { - request := testClient.ListOfferings(testCtx, testProjectId) + request := testClient.DefaultAPI.ListOfferings(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +62,7 @@ func fixtureRequest(mods ...func(request *redis.ApiListOfferingsRequest)) redis. func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -113,48 +117,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -177,7 +140,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), + cmp.AllowUnexported(tt.expectedRequest, redis.DefaultAPIService{}), cmpopts.EquateComparable(testCtx), ) if diff != "" { @@ -212,11 +175,10 @@ func Test_outputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.plans); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/redis/redis.go b/internal/cmd/redis/redis.go index 4a45e9c33..e0716339b 100644 --- a/internal/cmd/redis/redis.go +++ b/internal/cmd/redis/redis.go @@ -1,17 +1,17 @@ package redis import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/credentials" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/redis/plans" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "redis", Short: "Provides functionality for Redis", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(plans.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 750bdfa9a..72d41c1c9 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -2,11 +2,10 @@ package cmd import ( "fmt" - "os" "strings" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" affinityGroups "github.com/stackitcloud/stackit-cli/internal/cmd/affinity-groups" "github.com/stackitcloud/stackit-cli/internal/cmd/auth" @@ -17,8 +16,10 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/git" "github.com/stackitcloud/stackit-cli/internal/cmd/image" keypair "github.com/stackitcloud/stackit-cli/internal/cmd/key-pair" + "github.com/stackitcloud/stackit-cli/internal/cmd/kms" loadbalancer "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer" "github.com/stackitcloud/stackit-cli/internal/cmd/logme" + "github.com/stackitcloud/stackit-cli/internal/cmd/logs" "github.com/stackitcloud/stackit-cli/internal/cmd/mariadb" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex" "github.com/stackitcloud/stackit-cli/internal/cmd/network" @@ -51,20 +52,22 @@ import ( "github.com/spf13/viper" ) -func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { +func NewRootCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "stackit", Short: "Manage STACKIT resources using the command line", - Long: "Manage STACKIT resources using the command line.\nThis CLI is in a BETA state.\nMore services and functionality will be supported soon. Your feedback is appreciated!", + Long: "Manage STACKIT resources using the command line.\nYour feedback is appreciated!", Args: args.NoArgs, SilenceErrors: true, // Error is beautified in a custom way before being printed SilenceUsage: true, DisableAutoGenTag: true, PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - p.Cmd = cmd - p.Verbosity = print.Level(globalflags.Parse(p, cmd).Verbosity) + p := params.Printer + globalFlags := globalflags.Parse(p, cmd) + p.Verbosity = print.Level(globalFlags.Verbosity) + p.AssumeYes = globalFlags.AssumeYes - argsString := print.BuildDebugStrFromSlice(os.Args) + argsString := print.BuildDebugStrFromSlice(params.Args) p.Debug(print.DebugLevel, "arguments: %s", argsString) configFilePath := viper.ConfigFileUsed() @@ -97,36 +100,41 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { return nil }, RunE: func(cmd *cobra.Command, _ []string) error { + p := params.Printer if flags.FlagToBoolValue(p, cmd, "version") { - p.Outputf("STACKIT CLI (beta)\n") + p.Outputf("STACKIT CLI\n") - parsedDate, err := time.Parse(time.RFC3339, date) + parsedDate, err := time.Parse(time.RFC3339, params.Date) if err != nil { - p.Outputf("Version: %s\n", version) + p.Outputf("Version: %s\n", params.CliVersion) return nil } - p.Outputf("Version: %s (%s)\n", version, parsedDate.Format(time.DateOnly)) + p.Outputf("Version: %s (%s)\n", params.CliVersion, parsedDate.Format(time.DateOnly)) return nil } return cmd.Help() }, } - cmd.SetOut(os.Stdout) + cmd.SetOut(params.Printer.StdOut) err := configureFlags(cmd) cobra.CheckErr(err) - addSubcommands(cmd, ¶ms.CmdParams{ - Printer: p, - CliVersion: version, - }) + addSubcommands(cmd, params) - // Cobra creates the help flag with "help for " as the description - // We want to override that message by capitalizing the first letter to match the other flag descriptions - // See spf13/cobra#480 traverseCommands(cmd, func(c *cobra.Command) { + // Cobra creates the help flag with "help for " as the description + // We want to override that message by capitalizing the first letter to match the other flag descriptions + // See spf13/cobra#480 c.Flags().BoolP("help", "h", false, fmt.Sprintf("Help for %q", c.CommandPath())) + + c.SetArgs(params.Args) + c.SetIn(params.Printer.StdIn) + c.SetOut(params.Printer.StdOut) + c.SetErr(params.Printer.StdErr) + + c.Flags().SetOutput(params.Printer.StdErr) }) beautifyUsageTemplate(cmd) @@ -160,7 +168,7 @@ func configureFlags(cmd *cobra.Command) error { return nil } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(auth.NewCmd(params)) cmd.AddCommand(configCmd.NewCmd(params)) cmd.AddCommand(beta.NewCmd(params)) @@ -168,6 +176,7 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(dns.NewCmd(params)) cmd.AddCommand(loadbalancer.NewCmd(params)) cmd.AddCommand(logme.NewCmd(params)) + cmd.AddCommand(logs.NewCmd(params)) cmd.AddCommand(mariadb.NewCmd(params)) cmd.AddCommand(mongodbflex.NewCmd(params)) cmd.AddCommand(objectstorage.NewCmd(params)) @@ -193,6 +202,7 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(quota.NewCmd(params)) cmd.AddCommand(affinityGroups.NewCmd(params)) cmd.AddCommand(git.NewCmd(params)) + cmd.AddCommand(kms.NewCmd(params)) } // traverseCommands calls f for c and all of its children. @@ -203,33 +213,30 @@ func traverseCommands(c *cobra.Command, f func(*cobra.Command)) { } } -func Execute(version, date string) { - p := print.NewPrinter() - cmd := NewRootCmd(version, date, p) - - // We need to set the printer and verbosity here because the - // PersistentPreRun is not called when the command is wrongly called - p.Cmd = cmd - p.Verbosity = print.InfoLevel +// Execute executes the RootCmd and returns true on success and false on failure. +func Execute(params *types.CmdParams) bool { + cmd := NewRootCmd(params) + p := params.Printer err := cmd.Execute() if err != nil { - err := beautifyUnknownAndMissingCommandsError(cmd, err) + err := beautifyUnknownAndMissingCommandsError(cmd, err, params.Args) p.Debug(print.ErrorLevel, "execute command: %v", err) p.Error("%s", err.Error()) - os.Exit(1) + return false } + return true } // Returns a more user-friendly error if the input error is due to unknown/missing subcommands (issue: https://github.com/spf13/cobra/issues/706) // // Otherwise, returns the input error unchanged -func beautifyUnknownAndMissingCommandsError(rootCmd *cobra.Command, cmdErr error) error { +func beautifyUnknownAndMissingCommandsError(rootCmd *cobra.Command, cmdErr error, args []string) error { // nolint:gocritic // args is a nice name despite shadowing if !strings.HasPrefix(cmdErr.Error(), "unknown flag") { return cmdErr } - cmd, unparsedInputs, err := rootCmd.Traverse(os.Args[1:]) + cmd, unparsedInputs, err := rootCmd.Traverse(args) if err != nil { return cmdErr } diff --git a/internal/cmd/root_test.go b/internal/cmd/root_test.go index 71861a46f..3d9ca271b 100644 --- a/internal/cmd/root_test.go +++ b/internal/cmd/root_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/spf13/cobra" + pkgErrors "github.com/stackitcloud/stackit-cli/internal/pkg/errors" ) @@ -57,7 +58,7 @@ func TestBeautifyUnknownAndMissingCommandsError(t *testing.T) { setupCmd() for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - actualError := beautifyUnknownAndMissingCommandsError(cmd, tt.inputError) + actualError := beautifyUnknownAndMissingCommandsError(cmd, tt.inputError, []string{"--something"}) if tt.isNotUnknownFlagError { if actualError.Error() != tt.expectedMsg { diff --git a/internal/cmd/secrets-manager/instance/create/create.go b/internal/cmd/secrets-manager/instance/create/create.go index cd0cfd175..39310121e 100644 --- a/internal/cmd/secrets-manager/instance/create/create.go +++ b/internal/cmd/secrets-manager/instance/create/create.go @@ -2,11 +2,12 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,8 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" "github.com/spf13/cobra" ) @@ -24,16 +23,26 @@ import ( const ( instanceNameFlag = "name" aclFlag = "acl" + + kmsKeyIdFlag = "kms-key-id" + kmsKeyringIdFlag = "kms-keyring-id" + kmsKeyVersionFlag = "kms-key-version" + kmsServiceAccountEmailFlag = "kms-service-account-email" ) type inputModel struct { *globalflags.GlobalFlagModel - InstanceName *string + InstanceName string Acls *[]string + + KmsKeyId *string + KmsKeyringId *string + KmsKeyVersion *int64 + KmsServiceAccountEmail *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Secrets Manager instance", @@ -46,11 +55,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { examples.NewExample( `Create a Secrets Manager instance with name "my-instance" and specify IP range which is allowed to access it`, `$ stackit secrets-manager instance create --name my-instance --acl 1.2.3.0/24`), + examples.NewExample( + `Create a Secrets Manager instance with name "my-instance" and configure KMS key options`, + `$ stackit secrets-manager instance create --name my-instance --kms-key-id key-id --kms-keyring-id keyring-id --kms-key-version 1 --kms-service-account-email my-service-account-1234567@sa.stackit.cloud`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,12 +79,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a Secrets Manager instance for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a Secrets Manager instance for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API to create instance @@ -81,7 +91,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create Secrets Manager instance: %w", err) } - instanceId := *resp.Id + instanceId := resp.Id // Call API to create ACLs for instance, if ACLs are provided if model.Acls != nil { @@ -106,54 +116,68 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") + cmd.Flags().String(kmsKeyIdFlag, "", "ID of the KMS key to use for encryption") + cmd.Flags().String(kmsKeyringIdFlag, "", "ID of the KMS key ring") + cmd.Flags().Int64(kmsKeyVersionFlag, 0, "Version of the KMS key") + cmd.Flags().String(kmsServiceAccountEmailFlag, "", "Service account email for KMS access") + err := flags.MarkFlagsRequired(cmd, instanceNameFlag) cobra.CheckErr(err) + + cmd.MarkFlagsRequiredTogether(kmsKeyIdFlag, kmsKeyringIdFlag, kmsKeyVersionFlag, kmsServiceAccountEmailFlag) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} } model := inputModel{ - GlobalFlagModel: globalFlags, - InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), - Acls: flags.FlagToStringSlicePointer(p, cmd, aclFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + GlobalFlagModel: globalFlags, + InstanceName: flags.FlagToStringValue(p, cmd, instanceNameFlag), + Acls: flags.FlagToStringSlicePointer(p, cmd, aclFlag), + KmsKeyId: flags.FlagToStringPointer(p, cmd, kmsKeyIdFlag), + KmsKeyringId: flags.FlagToStringPointer(p, cmd, kmsKeyringIdFlag), + KmsKeyVersion: flags.FlagToInt64Pointer(p, cmd, kmsKeyVersionFlag), + KmsServiceAccountEmail: flags.FlagToStringPointer(p, cmd, kmsServiceAccountEmailFlag), } + p.DebugInputModel(model) return &model, nil } func buildCreateInstanceRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiCreateInstanceRequest { - req := apiClient.CreateInstance(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreateInstance(ctx, model.ProjectId) - req = req.CreateInstancePayload(secretsmanager.CreateInstancePayload{ + payload := secretsmanager.CreateInstancePayload{ Name: model.InstanceName, - }) + } + + if model.KmsKeyId != nil && model.KmsKeyringId != nil && model.KmsKeyVersion != nil && model.KmsServiceAccountEmail != nil { + payload.KmsKey = &secretsmanager.KmsKeyPayload{ + KeyId: *model.KmsKeyId, + KeyRingId: *model.KmsKeyringId, + KeyVersion: *model.KmsKeyVersion, + ServiceAccountEmail: *model.KmsServiceAccountEmail, + } + } + + req = req.CreateInstancePayload(payload) return req } func buildUpdateACLsRequest(ctx context.Context, model *inputModel, instanceId string, apiClient *secretsmanager.APIClient) secretsmanager.ApiUpdateACLsRequest { - req := apiClient.UpdateACLs(ctx, model.ProjectId, instanceId) + req := apiClient.DefaultAPI.UpdateACLs(ctx, model.ProjectId, instanceId) cidrs := make([]secretsmanager.UpdateACLPayload, len(*model.Acls)) for i, acl := range *model.Acls { - cidrs[i] = secretsmanager.UpdateACLPayload{Cidr: utils.Ptr(acl)} + cidrs[i] = secretsmanager.UpdateACLPayload{Cidr: acl} } - req = req.UpdateACLsPayload(secretsmanager.UpdateACLsPayload{Cidrs: &cidrs}) + req = req.UpdateACLsPayload(secretsmanager.UpdateACLsPayload{Cidrs: cidrs}) return req } @@ -163,25 +187,8 @@ func outputResult(p *print.Printer, outputFormat, projectLabel, instanceId strin return fmt.Errorf("instance is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instance, "", " ") - if err != nil { - return fmt.Errorf("marshal Secrets Manager instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Secrets Manager instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, instance, func() error { p.Outputf("Created instance for project %q. Instance ID: %s\n", projectLabel, instanceId) return nil - } + }) } diff --git a/internal/cmd/secrets-manager/instance/create/create_test.go b/internal/cmd/secrets-manager/instance/create/create_test.go index 02a895c01..930730c70 100644 --- a/internal/cmd/secrets-manager/instance/create/create_test.go +++ b/internal/cmd/secrets-manager/instance/create/create_test.go @@ -7,11 +7,12 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,10 +20,19 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() +const ( + testKmsKeyId = "key-id" + testKmsKeyringId = "keyring-id" + testKmsKeyVersion = int64(1) + testKmsServiceAccountEmail = "my-service-account-1234567@sa.stackit.cloud" +) + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ projectIdFlag: testProjectId, @@ -41,7 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr("example"), + InstanceName: "example", Acls: utils.Ptr([]string{"198.51.100.14/24"}), } for _, mod := range mods { @@ -51,9 +61,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiCreateInstanceRequest)) secretsmanager.ApiCreateInstanceRequest { - request := testClient.CreateInstance(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateInstance(testCtx, testProjectId) request = request.CreateInstancePayload(secretsmanager.CreateInstancePayload{ - Name: utils.Ptr("example"), + Name: "example", }) for _, mod := range mods { mod(&request) @@ -62,11 +72,11 @@ func fixtureRequest(mods ...func(request *secretsmanager.ApiCreateInstanceReques } func fixtureUpdateACLsRequest(mods ...func(request *secretsmanager.ApiUpdateACLsRequest)) secretsmanager.ApiUpdateACLsRequest { - request := testClient.UpdateACLs(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.UpdateACLs(testCtx, testProjectId, testInstanceId) request = request.UpdateACLsPayload(secretsmanager.UpdateACLsPayload{ - Cidrs: utils.Ptr([]secretsmanager.UpdateACLPayload{ - {Cidr: utils.Ptr("198.51.100.14/24")}, - })}) + Cidrs: []secretsmanager.UpdateACLPayload{ + {Cidr: "198.51.100.14/24"}, + }}) for _, mod := range mods { mod(&request) @@ -77,6 +87,7 @@ func fixtureUpdateACLsRequest(mods ...func(request *secretsmanager.ApiUpdateACLs func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -106,7 +117,7 @@ func TestParseInput(t *testing.T) { ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, }, - InstanceName: utils.Ptr(""), + InstanceName: "", Acls: &[]string{}, }, }, @@ -159,6 +170,24 @@ func TestParseInput(t *testing.T) { *model.Acls = append(*model.Acls, "1.2.3.4/32") }), }, + { + description: "kms flags", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, aclFlag) + flagValues[kmsKeyIdFlag] = testKmsKeyId + flagValues[kmsKeyringIdFlag] = testKmsKeyringId + flagValues[kmsKeyVersionFlag] = "1" + flagValues[kmsServiceAccountEmailFlag] = testKmsServiceAccountEmail + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Acls = nil + model.KmsKeyId = utils.Ptr(testKmsKeyId) + model.KmsKeyringId = utils.Ptr(testKmsKeyringId) + model.KmsKeyVersion = utils.Ptr(testKmsKeyVersion) + model.KmsServiceAccountEmail = utils.Ptr(testKmsServiceAccountEmail) + }), + }, { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { @@ -184,56 +213,9 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.aclValues { - err := cmd.Flags().Set(aclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", aclFlag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + aclFlag: tt.aclValues, + }, tt.isValid) }) } } @@ -249,6 +231,28 @@ func TestBuildCreateInstanceRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), }, + { + description: "with kms", + model: fixtureInputModel(func(model *inputModel) { + model.Acls = nil + model.KmsKeyId = utils.Ptr(testKmsKeyId) + model.KmsKeyringId = utils.Ptr(testKmsKeyringId) + model.KmsKeyVersion = utils.Ptr(testKmsKeyVersion) + model.KmsServiceAccountEmail = utils.Ptr(testKmsServiceAccountEmail) + }), + expectedRequest: fixtureRequest(func(request *secretsmanager.ApiCreateInstanceRequest) { + payload := secretsmanager.CreateInstancePayload{ + Name: "example", + KmsKey: &secretsmanager.KmsKeyPayload{ + KeyId: testKmsKeyId, + KeyRingId: testKmsKeyringId, + KeyVersion: testKmsKeyVersion, + ServiceAccountEmail: testKmsServiceAccountEmail, + }, + } + *request = request.CreateInstancePayload(payload) + }), + }, } for _, tt := range tests { @@ -282,10 +286,10 @@ func TestBuildCreateACLRequests(t *testing.T) { *model.Acls = append(*model.Acls, "1.2.3.4/32") }), expectedRequest: fixtureUpdateACLsRequest().UpdateACLsPayload(secretsmanager.UpdateACLsPayload{ - Cidrs: utils.Ptr([]secretsmanager.UpdateACLPayload{ - {Cidr: utils.Ptr("198.51.100.14/24")}, - {Cidr: utils.Ptr("1.2.3.4/32")}, - })}), + Cidrs: []secretsmanager.UpdateACLPayload{ + {Cidr: "198.51.100.14/24"}, + {Cidr: "1.2.3.4/32"}, + }}), }, } @@ -329,11 +333,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.instanceId, tt.args.instance); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.instanceId, tt.args.instance); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/secrets-manager/instance/delete/delete.go b/internal/cmd/secrets-manager/instance/delete/delete.go index 121d5427d..792dffdb5 100644 --- a/internal/cmd/secrets-manager/instance/delete/delete.go +++ b/internal/cmd/secrets-manager/instance/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" secretsmanagerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) const ( @@ -26,7 +28,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", instanceIdArg), Short: "Deletes a Secrets Manager instance", @@ -50,18 +52,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := secretsmanagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := secretsmanagerUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete instance %q? (This cannot be undone)", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -91,19 +91,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiDeleteInstanceRequest { - req := apiClient.DeleteInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.DeleteInstance(ctx, model.ProjectId, model.InstanceId) return req } diff --git a/internal/cmd/secrets-manager/instance/delete/delete_test.go b/internal/cmd/secrets-manager/instance/delete/delete_test.go index fe6eae032..902073fa8 100644 --- a/internal/cmd/secrets-manager/instance/delete/delete_test.go +++ b/internal/cmd/secrets-manager/instance/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiDeleteInstanceRequest)) secretsmanager.ApiDeleteInstanceRequest { - request := testClient.DeleteInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.DeleteInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -138,54 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/secrets-manager/instance/describe/describe.go b/internal/cmd/secrets-manager/instance/describe/describe.go index 3aed94fd5..3787c5fab 100644 --- a/internal/cmd/secrets-manager/instance/describe/describe.go +++ b/internal/cmd/secrets-manager/instance/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { InstanceId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", instanceIdArg), Short: "Shows details of a Secrets Manager instance", @@ -89,25 +88,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu InstanceId: instanceId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildGetInstanceRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiGetInstanceRequest { - req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.GetInstance(ctx, model.ProjectId, model.InstanceId) return req } func buildListACLsRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiListACLsRequest { - req := apiClient.ListACLs(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.ListACLs(ctx, model.ProjectId, model.InstanceId) return req } @@ -123,45 +114,41 @@ func outputResult(p *print.Printer, outputFormat string, instance *secretsmanage *secretsmanager.ListACLsResponse }{instance, aclList} - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(output, "", " ") - if err != nil { - return fmt.Errorf("marshal Secrets Manager instance: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Secrets Manager instance: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, output, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(instance.Id)) + table.AddRow("ID", instance.Id) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(instance.Name)) + table.AddRow("NAME", instance.Name) table.AddSeparator() - table.AddRow("STATE", utils.PtrString(instance.State)) + table.AddRow("STATE", instance.State) table.AddSeparator() - table.AddRow("SECRETS", utils.PtrString(instance.SecretCount)) + table.AddRow("SECRETS", instance.SecretCount) table.AddSeparator() - table.AddRow("ENGINE", utils.PtrString(instance.SecretsEngine)) + table.AddRow("ENGINE", instance.SecretsEngine) table.AddSeparator() - table.AddRow("CREATION DATE", utils.PtrString(instance.CreationStartDate)) + table.AddRow("CREATION DATE", instance.CreationStartDate) table.AddSeparator() + kmsKey := instance.KmsKey + if kmsKey != nil { + table.AddRow("KMS KEY ID", kmsKey.KeyId) + table.AddSeparator() + table.AddRow("KMS KEYRING ID", kmsKey.KeyRingId) + table.AddSeparator() + table.AddRow("KMS KEY VERSION", kmsKey.KeyVersion) + table.AddSeparator() + table.AddRow("KMS SERVICE ACCOUNT EMAIL", kmsKey.ServiceAccountEmail) + } // Only show ACL if it's present and not empty - if aclList.Acls != nil && len(*aclList.Acls) > 0 { + if len(aclList.Acls) > 0 { var cidrs []string - for _, acl := range *aclList.Acls { - cidrs = append(cidrs, *acl.Cidr) + for _, acl := range aclList.Acls { + cidrs = append(cidrs, acl.Cidr) } + if kmsKey != nil { + table.AddSeparator() + } table.AddRow("ACL", strings.Join(cidrs, ",")) } err := table.Display(p) @@ -170,5 +157,5 @@ func outputResult(p *print.Printer, outputFormat string, instance *secretsmanage } return nil - } + }) } diff --git a/internal/cmd/secrets-manager/instance/describe/describe_test.go b/internal/cmd/secrets-manager/instance/describe/describe_test.go index 2cadb3be2..19d6ba050 100644 --- a/internal/cmd/secrets-manager/instance/describe/describe_test.go +++ b/internal/cmd/secrets-manager/instance/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +19,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -58,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureGetInstanceRequest(mods ...func(request *secretsmanager.ApiGetInstanceRequest)) secretsmanager.ApiGetInstanceRequest { - request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.GetInstance(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -66,7 +68,7 @@ func fixtureGetInstanceRequest(mods ...func(request *secretsmanager.ApiGetInstan } func fixtureListACLsRequest(mods ...func(request *secretsmanager.ApiListACLsRequest)) secretsmanager.ApiListACLsRequest { - request := testClient.ListACLs(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListACLs(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -146,54 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -292,12 +247,26 @@ func TestOutputResult(t *testing.T) { }, wantErr: false, }, + { + name: "instance with kms key", + args: args{ + instance: &secretsmanager.Instance{ + KmsKey: &secretsmanager.KmsKeyPayload{ + KeyId: "key-id", + KeyRingId: "keyring-id", + KeyVersion: 1, + ServiceAccountEmail: "my-service-account-1234567@sa.stackit.cloud", + }, + }, + aclList: &secretsmanager.ListACLsResponse{}, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instance, tt.args.aclList); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instance, tt.args.aclList); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/secrets-manager/instance/instance.go b/internal/cmd/secrets-manager/instance/instance.go index 5617aeddc..8edeb55fc 100644 --- a/internal/cmd/secrets-manager/instance/instance.go +++ b/internal/cmd/secrets-manager/instance/instance.go @@ -1,19 +1,19 @@ package instance import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/create" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/list" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "instance", Short: "Provides functionality for Secrets Manager instances", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/secrets-manager/instance/list/list.go b/internal/cmd/secrets-manager/instance/list/list.go index 4098a7d76..a67e730ac 100644 --- a/internal/cmd/secrets-manager/instance/list/list.go +++ b/internal/cmd/secrets-manager/instance/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) const ( @@ -30,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Secrets Manager instances", @@ -47,9 +46,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Secrets Manager instances`, "$ stackit secrets-manager instance list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,7 +66,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("get Secrets Manager instances: %w", err) } - if resp.Instances == nil || len(*resp.Instances) == 0 { + if len(resp.Instances) == 0 { projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) if err != nil { params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) @@ -76,14 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { params.Printer.Info("No instances found for project %q\n", projectLabel) return nil } - instances := *resp.Instances // Truncate output - if model.Limit != nil && len(instances) > int(*model.Limit) { - instances = instances[:*model.Limit] + if model.Limit != nil && len(resp.Instances) > int(*model.Limit) { + resp.Instances = resp.Instances[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, instances) + return outputResult(params.Printer, model.OutputFormat, resp.Instances) }, } @@ -95,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -114,51 +112,26 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiListInstancesRequest { - req := apiClient.ListInstances(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListInstances(ctx, model.ProjectId) return req } func outputResult(p *print.Printer, outputFormat string, instances []secretsmanager.Instance) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(instances, "", " ") - if err != nil { - return fmt.Errorf("marshal Secrets Manager instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Secrets Manager instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, instances, func() error { table := tables.NewTable() table.SetHeader("ID", "NAME", "STATE", "SECRETS") for i := range instances { instance := instances[i] table.AddRow( - utils.PtrString(instance.Id), - utils.PtrString(instance.Name), - utils.PtrString(instance.State), - utils.PtrString(instance.SecretCount), + instance.Id, + instance.Name, + instance.State, + instance.SecretCount, ) } err := table.Display(p) @@ -167,5 +140,5 @@ func outputResult(p *print.Printer, outputFormat string, instances []secretsmana } return nil - } + }) } diff --git a/internal/cmd/secrets-manager/instance/list/list_test.go b/internal/cmd/secrets-manager/instance/list/list_test.go index 6ea677137..e512553e4 100644 --- a/internal/cmd/secrets-manager/instance/list/list_test.go +++ b/internal/cmd/secrets-manager/instance/list/list_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +20,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -50,7 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiListInstancesRequest)) secretsmanager.ApiListInstancesRequest { - request := testClient.ListInstances(testCtx, testProjectId) + request := testClient.DefaultAPI.ListInstances(testCtx, testProjectId) for _, mod := range mods { mod(&request) } @@ -60,6 +61,7 @@ func fixtureRequest(mods ...func(request *secretsmanager.ApiListInstancesRequest func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -124,48 +126,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -228,11 +189,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instances); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/secrets-manager/instance/update/update.go b/internal/cmd/secrets-manager/instance/update/update.go index b460360e2..7dc79fc56 100644 --- a/internal/cmd/secrets-manager/instance/update/update.go +++ b/internal/cmd/secrets-manager/instance/update/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,32 +18,53 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( instanceIdArg = "INSTANCE_ID" - aclFlag = "acl" + instanceNameFlag = "name" + aclFlag = "acl" + + kmsKeyIdFlag = "kms-key-id" + kmsKeyringIdFlag = "kms-keyring-id" + kmsKeyVersionFlag = "kms-key-version" + kmsServiceAccountEmailFlag = "kms-service-account-email" ) type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - Acls *[]string + InstanceName *string + Acls *[]string + + KmsKeyId *string + KmsKeyringId *string + KmsKeyVersion *int64 + KmsServiceAccountEmail *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", instanceIdArg), Short: "Updates a Secrets Manager instance", Long: "Updates a Secrets Manager instance.", Args: args.SingleArg(instanceIdArg, utils.ValidateUUID), Example: examples.Build( + examples.NewExample( + `Update the name of a Secrets Manager instance with ID "xxx"`, + "$ stackit secrets-manager instance update xxx --name my-new-name"), examples.NewExample( `Update the range of IPs allowed to access a Secrets Manager instance with ID "xxx"`, "$ stackit secrets-manager instance update xxx --acl 1.2.3.0/24"), + examples.NewExample( + `Update the name and ACLs of a Secrets Manager instance with ID "xxx"`, + "$ stackit secrets-manager instance update xxx --name my-new-name --acl 1.2.3.0/24"), + examples.NewExample( + `Update the KMS key settings of a Secrets Manager instance with ID "xxx"`, + "$ stackit secrets-manager instance update xxx --name my-instance --kms-key-id key-id --kms-keyring-id keyring-id --kms-key-version 1 --kms-service-account-email my-service-account-1234567@sa.stackit.cloud"), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -57,28 +79,45 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + existingInstanceName, err := secretsManagerUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) - instanceLabel = model.InstanceId + existingInstanceName = model.InstanceId + } + + prompt := fmt.Sprintf("Are you sure you want to update instance %q?", existingInstanceName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) + // Call API - execute UpdateInstance and/or UpdateACLs based on flags + if model.InstanceName != nil { + req, err := buildUpdateInstanceRequest(ctx, model, apiClient) if err != nil { - return err + return fmt.Errorf("unable to build update instance request: %w", err) + } + err = req.Execute() + if err != nil { + return fmt.Errorf("update Secrets Manager instance: %w", err) } } - // Call API - req := buildRequest(ctx, model, apiClient) - err = req.Execute() - if err != nil { - return fmt.Errorf("update Secrets Manager instance: %w", err) + if model.Acls != nil { + req := buildUpdateACLsRequest(ctx, model, apiClient) + err = req.Execute() + if err != nil { + if model.InstanceName != nil { + return fmt.Errorf(`the Secrets Manager instance was successfully updated, but the configuration of the ACLs failed. + +If you want to retry configuring the ACLs, you can do it via: + $ stackit secrets-manager instance update %s --acl %s`, model.InstanceId, *model.Acls) + } + return fmt.Errorf("update Secrets Manager instance ACLs: %w", err) + } } - params.Printer.Info("Updated instance %q\n", instanceLabel) + params.Printer.Info("Updated instance %q\n", existingInstanceName) return nil }, } @@ -87,7 +126,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { + cmd.Flags().StringP(instanceNameFlag, "n", "", "Instance name") cmd.Flags().Var(flags.CIDRSliceFlag(), aclFlag, "List of IP networks in CIDR notation which are allowed to access this instance") + + cmd.Flags().String(kmsKeyIdFlag, "", "ID of the KMS key to use for encryption") + cmd.Flags().String(kmsKeyringIdFlag, "", "ID of the KMS key ring") + cmd.Flags().Int64(kmsKeyVersionFlag, 0, "Version of the KMS key") + cmd.Flags().String(kmsServiceAccountEmailFlag, "", "Service account email for KMS access") + + cmd.MarkFlagsRequiredTogether(kmsKeyIdFlag, kmsKeyringIdFlag, kmsKeyVersionFlag, kmsServiceAccountEmailFlag) + cmd.MarkFlagsOneRequired(aclFlag, instanceNameFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -98,40 +146,58 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return nil, &cliErr.ProjectIdError{} } - acls := flags.FlagToStringSlicePointer(p, cmd, aclFlag) + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: instanceId, + InstanceName: flags.FlagToStringPointer(p, cmd, instanceNameFlag), + Acls: flags.FlagToStringSlicePointer(p, cmd, aclFlag), + KmsKeyId: flags.FlagToStringPointer(p, cmd, kmsKeyIdFlag), + KmsKeyringId: flags.FlagToStringPointer(p, cmd, kmsKeyringIdFlag), + KmsKeyVersion: flags.FlagToInt64Pointer(p, cmd, kmsKeyVersionFlag), + KmsServiceAccountEmail: flags.FlagToStringPointer(p, cmd, kmsServiceAccountEmailFlag), + } - if acls == nil { - return nil, &cliErr.EmptyUpdateError{} + if model.KmsKeyId != nil && model.InstanceName == nil { + return nil, fmt.Errorf("--name is required when using KMS flags") } - model := inputModel{ - GlobalFlagModel: globalFlags, - InstanceId: instanceId, - Acls: acls, + p.DebugInputModel(model) + return &model, nil +} + +func buildUpdateInstanceRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) (secretsmanager.ApiUpdateInstanceRequest, error) { + if model.InstanceName == nil { + return secretsmanager.ApiUpdateInstanceRequest{}, fmt.Errorf("instanceName must not be null") } + req := apiClient.DefaultAPI.UpdateInstance(ctx, model.ProjectId, model.InstanceId) - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + payload := secretsmanager.UpdateInstancePayload{ + Name: *model.InstanceName, + } + + if model.KmsKeyId != nil && model.KmsKeyringId != nil && model.KmsKeyVersion != nil && model.KmsServiceAccountEmail != nil { + payload.KmsKey = &secretsmanager.KmsKeyPayload{ + KeyId: *model.KmsKeyId, + KeyRingId: *model.KmsKeyringId, + KeyVersion: *model.KmsKeyVersion, + ServiceAccountEmail: *model.KmsServiceAccountEmail, } } + req = req.UpdateInstancePayload(payload) - return &model, nil + return req, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiUpdateACLsRequest { - req := apiClient.UpdateACLs(ctx, model.ProjectId, model.InstanceId) +func buildUpdateACLsRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiUpdateACLsRequest { + req := apiClient.DefaultAPI.UpdateACLs(ctx, model.ProjectId, model.InstanceId) cidrs := []secretsmanager.UpdateACLPayload{} for _, acl := range *model.Acls { - cidrs = append(cidrs, secretsmanager.UpdateACLPayload{Cidr: utils.Ptr(acl)}) + cidrs = append(cidrs, secretsmanager.UpdateACLPayload{Cidr: acl}) } - req = req.UpdateACLsPayload(secretsmanager.UpdateACLsPayload{Cidrs: &cidrs}) + req = req.UpdateACLsPayload(secretsmanager.UpdateACLsPayload{Cidrs: cidrs}) return req } diff --git a/internal/cmd/secrets-manager/instance/update/update_test.go b/internal/cmd/secrets-manager/instance/update/update_test.go index 1a60db361..2ddd2a8d1 100644 --- a/internal/cmd/secrets-manager/instance/update/update_test.go +++ b/internal/cmd/secrets-manager/instance/update/update_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( @@ -25,13 +24,23 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var ( testProjectId = uuid.NewString() testInstanceId = uuid.NewString() ) +const ( + testInstanceName = "test-instance" + testKmsKeyId = "key-id" + testKmsKeyringId = "keyring-id" + testKmsKeyVersion = int64(1) + testKmsServiceAccountEmail = "my-service-account-1234567@sa.stackit.cloud" +) + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testInstanceId, @@ -69,11 +78,29 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiUpdateACLsRequest)) secretsmanager.ApiUpdateACLsRequest { - request := testClient.UpdateACLs(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.UpdateACLs(testCtx, testProjectId, testInstanceId) request = request.UpdateACLsPayload(secretsmanager.UpdateACLsPayload{ - Cidrs: utils.Ptr([]secretsmanager.UpdateACLPayload{ - {Cidr: utils.Ptr(testACL1)}, - })}) + Cidrs: []secretsmanager.UpdateACLPayload{ + {Cidr: testACL1}, + }}) + + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureUpdateInstanceRequest(mods ...func(request *secretsmanager.ApiUpdateInstanceRequest)) secretsmanager.ApiUpdateInstanceRequest { + request := testClient.DefaultAPI.UpdateInstance(testCtx, testProjectId, testInstanceId) + request = request.UpdateInstancePayload(secretsmanager.UpdateInstancePayload{ + Name: testInstanceName, + KmsKey: &secretsmanager.KmsKeyPayload{ + KeyId: testKmsKeyId, + KeyRingId: testKmsKeyringId, + KeyVersion: testKmsKeyVersion, + ServiceAccountEmail: testKmsServiceAccountEmail, + }, + }) for _, mod := range mods { mod(&request) @@ -110,13 +137,7 @@ func TestParseInput(t *testing.T) { isValid: false, }, { - description: "no flag values", - argValues: fixtureArgValues(), - flagValues: map[string]string{}, - isValid: false, - }, - { - description: "required flags only (no values to update)", + description: "no update flags", argValues: fixtureArgValues(), flagValues: map[string]string{ projectIdFlag: testProjectId, @@ -171,6 +192,27 @@ func TestParseInput(t *testing.T) { flagValues: fixtureFlagValues(), isValid: false, }, + { + description: "kms key id without other required kms flags", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + kmsKeyIdFlag: "key-id", + }, + isValid: false, + }, + { + description: "kms flags without name flag", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + kmsKeyIdFlag: "key-id", + kmsKeyringIdFlag: "keyring-id", + kmsKeyVersionFlag: "1", + kmsServiceAccountEmailFlag: "my-service-account-1234567@sa.stackit.cloud", + }, + isValid: false, + }, { description: "repeated acl flags", argValues: fixtureArgValues(), @@ -194,72 +236,85 @@ func TestParseInput(t *testing.T) { ) }), }, + { + description: "name flag only", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + instanceNameFlag: "updated-name", + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Acls = nil + model.InstanceName = utils.Ptr("updated-name") + }), + }, + { + description: "name and acl flags", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + instanceNameFlag: testInstanceName, + aclFlag: testACL1, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.InstanceName = utils.Ptr(testInstanceName) + }), + }, + { + description: "kms flags with name", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + instanceNameFlag: testInstanceName, + kmsKeyIdFlag: testKmsKeyId, + kmsKeyringIdFlag: testKmsKeyringId, + kmsKeyVersionFlag: "1", + kmsServiceAccountEmailFlag: testKmsServiceAccountEmail, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Acls = nil + model.InstanceName = utils.Ptr(testInstanceName) + model.KmsKeyId = utils.Ptr(testKmsKeyId) + model.KmsKeyringId = utils.Ptr(testKmsKeyringId) + model.KmsKeyVersion = utils.Ptr(testKmsKeyVersion) + model.KmsServiceAccountEmail = utils.Ptr(testKmsServiceAccountEmail) + }), + }, + { + description: "name, acl and kms flags together", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + projectIdFlag: testProjectId, + instanceNameFlag: testInstanceName, + aclFlag: testACL1, + kmsKeyIdFlag: testKmsKeyId, + kmsKeyringIdFlag: testKmsKeyringId, + kmsKeyVersionFlag: "1", + kmsServiceAccountEmailFlag: testKmsServiceAccountEmail, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.InstanceName = utils.Ptr(testInstanceName) + model.KmsKeyId = utils.Ptr(testKmsKeyId) + model.KmsKeyringId = utils.Ptr(testKmsKeyringId) + model.KmsKeyVersion = utils.Ptr(testKmsKeyVersion) + model.KmsServiceAccountEmail = utils.Ptr(testKmsServiceAccountEmail) + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - for _, value := range tt.aclValues { - err := cmd.Flags().Set(aclFlag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", aclFlag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInputWithAdditionalFlags(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, map[string][]string{ + aclFlag: tt.aclValues, + }, tt.isValid) }) } } -func TestBuildRequest(t *testing.T) { +func TestBuildUpdateACLsRequest(t *testing.T) { tests := []struct { description string model *inputModel @@ -276,16 +331,73 @@ func TestBuildRequest(t *testing.T) { *model.Acls = append(*model.Acls, testACL2) }), expectedRequest: fixtureRequest().UpdateACLsPayload(secretsmanager.UpdateACLsPayload{ - Cidrs: utils.Ptr([]secretsmanager.UpdateACLPayload{ - {Cidr: utils.Ptr(testACL1)}, - {Cidr: utils.Ptr(testACL2)}, - })}), + Cidrs: []secretsmanager.UpdateACLPayload{ + {Cidr: testACL1}, + {Cidr: testACL2}, + }}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, tt.model, testClient) + request := buildUpdateACLsRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildUpdateInstanceRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest secretsmanager.ApiUpdateInstanceRequest + wantErr bool + }{ + { + description: "with name only", + model: fixtureInputModel(func(model *inputModel) { + model.Acls = nil + model.InstanceName = utils.Ptr(testInstanceName) + }), + expectedRequest: testClient.DefaultAPI.UpdateInstance(testCtx, testProjectId, testInstanceId). + UpdateInstancePayload(secretsmanager.UpdateInstancePayload{ + Name: testInstanceName, + }), + }, + { + description: "with KMS settings", + model: fixtureInputModel(func(model *inputModel) { + model.Acls = nil + model.InstanceName = utils.Ptr(testInstanceName) + model.KmsKeyId = utils.Ptr(testKmsKeyId) + model.KmsKeyringId = utils.Ptr(testKmsKeyringId) + model.KmsKeyVersion = utils.Ptr(testKmsKeyVersion) + model.KmsServiceAccountEmail = utils.Ptr(testKmsServiceAccountEmail) + }), + expectedRequest: fixtureUpdateInstanceRequest(), + }, + { + description: "nil instance name", + model: fixtureInputModel(func(model *inputModel) { + model.InstanceName = nil + }), + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request, err := buildUpdateInstanceRequest(testCtx, tt.model, testClient) + if (err != nil) != tt.wantErr { + t.Fatalf("buildUpdateInstanceRequest() error = %v, wantErr %v", err, tt.wantErr) + } diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), diff --git a/internal/cmd/secrets-manager/secrets_manager.go b/internal/cmd/secrets-manager/secrets_manager.go index 35745dfde..eb5632c4f 100644 --- a/internal/cmd/secrets-manager/secrets_manager.go +++ b/internal/cmd/secrets-manager/secrets_manager.go @@ -1,16 +1,16 @@ package secretsmanager import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/user" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "secrets-manager", Short: "Provides functionality for Secrets Manager", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(instance.NewCmd(params)) cmd.AddCommand(user.NewCmd(params)) } diff --git a/internal/cmd/secrets-manager/user/create/create.go b/internal/cmd/secrets-manager/user/create/create.go index 5e8e3e25f..b186eaf9f 100644 --- a/internal/cmd/secrets-manager/user/create/create.go +++ b/internal/cmd/secrets-manager/user/create/create.go @@ -2,11 +2,10 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,10 +14,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" secretsManagerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/utils" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( @@ -31,11 +29,11 @@ type inputModel struct { *globalflags.GlobalFlagModel InstanceId string - Description *string - Write *bool + Description string + Write bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Secrets Manager user", @@ -53,9 +51,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit secrets-manager user create --instance-id xxx --write"), ), Args: args.NoArgs, - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -66,18 +64,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a user for instance %q?", instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -104,7 +100,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -113,24 +109,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), - Description: utils.Ptr(flags.FlagToStringValue(p, cmd, descriptionFlag)), - Write: utils.Ptr(flags.FlagToBoolValue(p, cmd, writeFlag)), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Description: flags.FlagToStringValue(p, cmd, descriptionFlag), + Write: flags.FlagToBoolValue(p, cmd, writeFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiCreateUserRequest { - req := apiClient.CreateUser(ctx, model.ProjectId, model.InstanceId) + req := apiClient.DefaultAPI.CreateUser(ctx, model.ProjectId, model.InstanceId) req = req.CreateUserPayload(secretsmanager.CreateUserPayload{ Description: model.Description, Write: model.Write, @@ -143,30 +131,13 @@ func outputResult(p *print.Printer, outputFormat, instanceLabel string, user *se return fmt.Errorf("user is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal Secrets Manager user: %w", err) - } - p.Outputln(string(details)) + return p.OutputResult(outputFormat, user, func() error { + p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, user.Id) + p.Outputf("Username: %s\n", user.Username) + p.Outputf("Password: %s\n", user.Password) + p.Outputf("Description: %s\n", user.Description) + p.Outputf("Write Access: %t\n", user.Write) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Secrets Manager user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, utils.PtrString(user.Id)) - p.Outputf("Username: %s\n", utils.PtrString(user.Username)) - p.Outputf("Password: %s\n", utils.PtrString(user.Password)) - p.Outputf("Description: %s\n", utils.PtrString(user.Description)) - p.Outputf("Write Access: %s\n", utils.PtrString(user.Write)) - - return nil - } + }) } diff --git a/internal/cmd/secrets-manager/user/create/create_test.go b/internal/cmd/secrets-manager/user/create/create_test.go index a64b826ba..591cf991e 100644 --- a/internal/cmd/secrets-manager/user/create/create_test.go +++ b/internal/cmd/secrets-manager/user/create/create_test.go @@ -4,16 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +19,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -45,8 +45,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, InstanceId: testInstanceId, - Description: utils.Ptr("sample description"), - Write: utils.Ptr(false), + Description: "sample description", + Write: false, } for _, mod := range mods { mod(model) @@ -55,10 +55,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiCreateUserRequest)) secretsmanager.ApiCreateUserRequest { - request := testClient.CreateUser(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.CreateUser(testCtx, testProjectId, testInstanceId) request = request.CreateUserPayload(secretsmanager.CreateUserPayload{ - Description: utils.Ptr("sample description"), - Write: utils.Ptr(false), + Description: "sample description", + Write: false, }) for _, mod := range mods { @@ -70,6 +70,7 @@ func fixtureRequest(mods ...func(request *secretsmanager.ApiCreateUserRequest)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -88,7 +89,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Description = utils.Ptr("") + model.Description = "" }), }, { @@ -146,55 +147,14 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Write = utils.Ptr(true) + model.Write = true }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -251,11 +211,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.instanceLabel, tt.args.user); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/secrets-manager/user/delete/delete.go b/internal/cmd/secrets-manager/user/delete/delete.go index 571ef6130..e5bdd4a19 100644 --- a/internal/cmd/secrets-manager/user/delete/delete.go +++ b/internal/cmd/secrets-manager/user/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( @@ -32,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", userIdArg), Short: "Deletes a Secrets Manager user", @@ -59,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := secretsManagerUtils.GetUserLabel(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId) + userLabel, err := secretsManagerUtils.GetUserLabel(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user label: %v", err) userLabel = fmt.Sprintf("%q", model.UserId) } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete user %s of instance %q? (This cannot be undone)", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete user %s of instance %q? (This cannot be undone)", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -115,19 +114,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiDeleteUserRequest { - req := apiClient.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.DeleteUser(ctx, model.ProjectId, model.InstanceId, model.UserId) return req } diff --git a/internal/cmd/secrets-manager/user/delete/delete_test.go b/internal/cmd/secrets-manager/user/delete/delete_test.go index 0827ebf88..c105aab13 100644 --- a/internal/cmd/secrets-manager/user/delete/delete_test.go +++ b/internal/cmd/secrets-manager/user/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -61,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiDeleteUserRequest)) secretsmanager.ApiDeleteUserRequest { - request := testClient.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId) + request := testClient.DefaultAPI.DeleteUser(testCtx, testProjectId, testInstanceId, testUserId) for _, mod := range mods { mod(&request) } @@ -153,54 +154,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/secrets-manager/user/describe/describe.go b/internal/cmd/secrets-manager/user/describe/describe.go index b11949417..61033277e 100644 --- a/internal/cmd/secrets-manager/user/describe/describe.go +++ b/internal/cmd/secrets-manager/user/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { UserId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", userIdArg), Short: "Shows details of a Secrets Manager user", @@ -68,7 +67,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("get Secrets Manager user: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } @@ -97,56 +96,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiGetUserRequest { - req := apiClient.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.GetUser(ctx, model.ProjectId, model.InstanceId, model.UserId) return req } -func outputResult(p *print.Printer, outputFormat string, user secretsmanager.User) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(user, "", " ") - if err != nil { - return fmt.Errorf("marshal Secrets Manager user: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Secrets Manager user: %w", err) - } - p.Outputln(string(details)) - - return nil - default: +func outputResult(p *print.Printer, outputFormat string, user *secretsmanager.User) error { + return p.OutputResult(outputFormat, user, func() error { table := tables.NewTable() - table.AddRow("ID", utils.PtrString(user.Id)) + table.AddRow("ID", user.Id) table.AddSeparator() - table.AddRow("USERNAME", utils.PtrString(user.Username)) + table.AddRow("USERNAME", user.Username) table.AddSeparator() - if user.Description != nil && *user.Description != "" { - table.AddRow("DESCRIPTION", *user.Description) + if user.Description != "" { + table.AddRow("DESCRIPTION", user.Description) table.AddSeparator() } - if user.Password != nil && *user.Password != "" { - table.AddRow("PASSWORD", *user.Password) + if user.Password != "" { + table.AddRow("PASSWORD", user.Password) table.AddSeparator() } - table.AddRow("WRITE ACCESS", utils.PtrString(user.Write)) + table.AddRow("WRITE ACCESS", user.Write) err := table.Display(p) if err != nil { @@ -154,5 +128,5 @@ func outputResult(p *print.Printer, outputFormat string, user secretsmanager.Use } return nil - } + }) } diff --git a/internal/cmd/secrets-manager/user/describe/describe_test.go b/internal/cmd/secrets-manager/user/describe/describe_test.go index 53c14c586..f62123748 100644 --- a/internal/cmd/secrets-manager/user/describe/describe_test.go +++ b/internal/cmd/secrets-manager/user/describe/describe_test.go @@ -4,14 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +20,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -61,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiGetUserRequest)) secretsmanager.ApiGetUserRequest { - request := testClient.GetUser(testCtx, testProjectId, testInstanceId, testUserId) + request := testClient.DefaultAPI.GetUser(testCtx, testProjectId, testInstanceId, testUserId) for _, mod := range mods { mod(&request) } @@ -165,54 +168,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -268,11 +224,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.user); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, utils.Ptr(tt.args.user)); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/secrets-manager/user/list/list.go b/internal/cmd/secrets-manager/user/list/list.go index 7040d69a6..81fb7b25b 100644 --- a/internal/cmd/secrets-manager/user/list/list.go +++ b/internal/cmd/secrets-manager/user/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/client" secretsManagerUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/secrets-manager/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all Secrets Manager users", @@ -50,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 Secrets Manager users with ID "xxx"`, "$ stackit secrets-manager user list --instance-id xxx --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,8 +68,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get Secrets Manager users: %w", err) } - if resp.Users == nil || len(*resp.Users) == 0 { - instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId) + if len(resp.Users) == 0 { + instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, *model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = *model.InstanceId @@ -78,14 +77,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { params.Printer.Info("No users found for instance %q\n", instanceLabel) return nil } - users := *resp.Users // Truncate output - if model.Limit != nil && len(users) > int(*model.Limit) { - users = users[:*model.Limit] + if model.Limit != nil && len(resp.Users) > int(*model.Limit) { + resp.Users = resp.Users[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, users) + return outputResult(params.Printer, model.OutputFormat, (resp.Users)) }, } @@ -101,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -121,51 +119,26 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) secretsmanager.ApiListUsersRequest { - req := apiClient.ListUsers(ctx, model.ProjectId, *model.InstanceId) + req := apiClient.DefaultAPI.ListUsers(ctx, model.ProjectId, *model.InstanceId) return req } func outputResult(p *print.Printer, outputFormat string, users []secretsmanager.User) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(users, "", " ") - if err != nil { - return fmt.Errorf("marshal Secrets Manager user list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Secrets Manager user list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, users, func() error { table := tables.NewTable() table.SetHeader("ID", "USERNAME", "DESCRIPTION", "WRITE ACCESS") for i := range users { user := users[i] table.AddRow( - utils.PtrString(user.Id), - utils.PtrString(user.Username), - utils.PtrString(user.Description), - utils.PtrString(user.Write), + user.Id, + user.Username, + user.Description, + user.Write, ) } err := table.Display(p) @@ -174,5 +147,5 @@ func outputResult(p *print.Printer, outputFormat string, users []secretsmanager. } return nil - } + }) } diff --git a/internal/cmd/secrets-manager/user/list/list_test.go b/internal/cmd/secrets-manager/user/list/list_test.go index d2de82297..bba7c86d4 100644 --- a/internal/cmd/secrets-manager/user/list/list_test.go +++ b/internal/cmd/secrets-manager/user/list/list_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +20,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() @@ -53,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiListUsersRequest)) secretsmanager.ApiListUsersRequest { - request := testClient.ListUsers(testCtx, testProjectId, testInstanceId) + request := testClient.DefaultAPI.ListUsers(testCtx, testProjectId, testInstanceId) for _, mod := range mods { mod(&request) } @@ -63,6 +64,7 @@ func fixtureRequest(mods ...func(request *secretsmanager.ApiListUsersRequest)) s func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -131,48 +133,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -235,11 +196,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.users); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.users); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/secrets-manager/user/update/update.go b/internal/cmd/secrets-manager/user/update/update.go index 07e9b6edd..efffd6fe3 100644 --- a/internal/cmd/secrets-manager/user/update/update.go +++ b/internal/cmd/secrets-manager/user/update/update.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) const ( @@ -36,7 +37,7 @@ type inputModel struct { DisableWrite *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", userIdArg), Short: "Updates the write privileges Secrets Manager user", @@ -63,24 +64,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + instanceLabel, err := secretsManagerUtils.GetInstanceName(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get instance name: %v", err) instanceLabel = model.InstanceId } - userLabel, err := secretsManagerUtils.GetUserLabel(ctx, apiClient, model.ProjectId, model.InstanceId, model.UserId) + userLabel, err := secretsManagerUtils.GetUserLabel(ctx, apiClient.DefaultAPI, model.ProjectId, model.InstanceId, model.UserId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get user label: %v", err) userLabel = fmt.Sprintf("%q", model.UserId) } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update user %s of instance %q?", userLabel, instanceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update user %s of instance %q?", userLabel, instanceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -137,20 +136,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UserId: userId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *secretsmanager.APIClient) (secretsmanager.ApiUpdateUserRequest, error) { - req := apiClient.UpdateUser(ctx, model.ProjectId, model.InstanceId, model.UserId) + req := apiClient.DefaultAPI.UpdateUser(ctx, model.ProjectId, model.InstanceId, model.UserId) var write bool diff --git a/internal/cmd/secrets-manager/user/update/update_test.go b/internal/cmd/secrets-manager/user/update/update_test.go index 5ae24676b..24ec6dd3b 100644 --- a/internal/cmd/secrets-manager/user/update/update_test.go +++ b/internal/cmd/secrets-manager/user/update/update_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +19,9 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &secretsmanager.APIClient{} +var testClient = &secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{}, +} var testProjectId = uuid.NewString() var testInstanceId = uuid.NewString() var testUserId = uuid.NewString() @@ -65,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *secretsmanager.ApiUpdateUserRequest)) secretsmanager.ApiUpdateUserRequest { - request := testClient.UpdateUser(testCtx, testProjectId, testInstanceId, testUserId) + request := testClient.DefaultAPI.UpdateUser(testCtx, testProjectId, testInstanceId, testUserId) request = request.UpdateUserPayload(secretsmanager.UpdateUserPayload{ Write: utils.Ptr(true), }) @@ -189,8 +190,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -230,7 +231,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flag groups: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return diff --git a/internal/cmd/secrets-manager/user/user.go b/internal/cmd/secrets-manager/user/user.go index ae4e1c90f..6f14e07dd 100644 --- a/internal/cmd/secrets-manager/user/user.go +++ b/internal/cmd/secrets-manager/user/user.go @@ -2,10 +2,11 @@ package user import ( "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/user/create" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/user/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/user/describe" @@ -13,7 +14,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/secrets-manager/user/update" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "user", Short: "Provides functionality for Secrets Manager users", @@ -25,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/security-group/create/create.go b/internal/cmd/security-group/create/create.go index 1e17b9002..b835d10b4 100644 --- a/internal/cmd/security-group/create/create.go +++ b/internal/cmd/security-group/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -28,13 +28,13 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - Labels *map[string]string + Labels map[string]any Description *string - Name *string + Name string Stateful *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates security groups", @@ -44,9 +44,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { examples.NewExample(`Create a named group`, `$ stackit security-group create --name my-new-group`), examples.NewExample(`Create a named group with labels`, `$ stackit security-group create --name my-new-group --labels label1=value1,label2=value2`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -57,12 +57,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create the security group %q?", *model.Name) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create the security group %q?", model.Name) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -73,7 +71,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create security group: %w", err) } - if err := outputResult(params.Printer, model.OutputFormat, *model.Name, *group); err != nil { + if err := outputResult(params.Printer, model.OutputFormat, model.Name, group); err != nil { return err } @@ -96,7 +94,7 @@ func configureFlags(cmd *cobra.Command) { } } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -105,31 +103,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - Name: &name, + Name: name, - Labels: flags.FlagToStringToStringPointer(p, cmd, labelsFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelsFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), Stateful: flags.FlagToBoolPointer(p, cmd, statefulFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateSecurityGroupRequest { - request := apiClient.CreateSecurityGroup(ctx, model.ProjectId) + request := apiClient.DefaultAPI.CreateSecurityGroup(ctx, model.ProjectId, model.Region) payload := iaas.CreateSecurityGroupPayload{ Description: model.Description, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, Name: model.Name, Stateful: model.Stateful, } @@ -137,26 +127,9 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return request.CreateSecurityGroupPayload(payload) } -func outputResult(p *print.Printer, outputFormat, name string, resp iaas.SecurityGroup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal security group: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal security group: %w", err) - } - p.Outputln(string(details)) - - return nil - default: +func outputResult(p *print.Printer, outputFormat, name string, resp *iaas.SecurityGroup) error { + return p.OutputResult(outputFormat, resp, func() error { p.Outputf("Created security group %q.\nSecurity Group ID %s\n", name, utils.PtrString(resp.Id)) return nil - } + }) } diff --git a/internal/cmd/security-group/create/create_test.go b/internal/cmd/security-group/create/create_test.go index 7b9cbd21e..986a6b0c0 100644 --- a/internal/cmd/security-group/create/create_test.go +++ b/internal/cmd/security-group/create/create_test.go @@ -7,24 +7,27 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testName = "new-security-group" testDescription = "a test description" - testLabels = map[string]string{ + testLabels = map[string]any{ "fooKey": "fooValue", "barKey": "barValue", "bazKey": "bazValue", @@ -34,7 +37,9 @@ var ( func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + descriptionFlag: testDescription, labelsFlag: "fooKey=fooValue,barKey=barValue,bazKey=bazValue", statefulFlag: "true", @@ -48,11 +53,15 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - Labels: &testLabels, - Description: &testDescription, - Name: &testName, - Stateful: &testStateful, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Labels: testLabels, + Description: &testDescription, + Name: testName, + Stateful: &testStateful, } for _, mod := range mods { mod(model) @@ -60,23 +69,13 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func toStringAnyMapPtr(m map[string]string) map[string]any { - if m == nil { - return nil - } - result := map[string]any{} - for k, v := range m { - result[k] = v - } - return result -} func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRequest)) iaas.ApiCreateSecurityGroupRequest { - request := testClient.CreateSecurityGroup(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateSecurityGroup(testCtx, testProjectId, testRegion) request = request.CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ Description: &testDescription, - Labels: utils.Ptr(toStringAnyMapPtr(testLabels)), - Name: &testName, + Labels: testLabels, + Name: testName, Rules: nil, Stateful: &testStateful, }) @@ -89,6 +88,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRequest)) i func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -107,21 +107,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -149,7 +149,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ + model.Labels = map[string]any{ "foo": "bar", } }), @@ -168,44 +168,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -227,10 +190,10 @@ func TestBuildRequest(t *testing.T) { model.Labels = nil }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateSecurityGroupRequest) { - *request = (*request).CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ + *request = request.CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ Description: &testDescription, Labels: nil, - Name: &testName, + Name: testName, Stateful: &testStateful, }) }), @@ -241,10 +204,10 @@ func TestBuildRequest(t *testing.T) { model.Stateful = utils.Ptr(false) }), expectedRequest: fixtureRequest(func(request *iaas.ApiCreateSecurityGroupRequest) { - *request = (*request).CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ + *request = request.CreateSecurityGroupPayload(iaas.CreateSecurityGroupPayload{ Description: &testDescription, - Labels: utils.Ptr(toStringAnyMapPtr(testLabels)), - Name: &testName, + Labels: testLabels, + Name: testName, Stateful: utils.Ptr(false), }) }), @@ -256,7 +219,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -282,11 +245,10 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.name, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.name, &tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/security-group/delete/delete.go b/internal/cmd/security-group/delete/delete.go index 329e4b026..6937998e9 100644 --- a/internal/cmd/security-group/delete/delete.go +++ b/internal/cmd/security-group/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { @@ -25,7 +27,7 @@ type inputModel struct { const groupIdArg = "GROUP_ID" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", groupIdArg), Short: "Deletes a security group", @@ -53,18 +55,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - groupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId) + groupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) if err != nil { params.Printer.Warn("get security group name: %v", err) groupLabel = model.SecurityGroupId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the security group %q for %q?", groupLabel, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the security group %q for %q?", groupLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -93,19 +93,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM SecurityGroupId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteSecurityGroupRequest { - request := apiClient.DeleteSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId) + request := apiClient.DefaultAPI.DeleteSecurityGroup(ctx, model.ProjectId, model.Region, model.SecurityGroupId) return request } diff --git a/internal/cmd/security-group/delete/delete_test.go b/internal/cmd/security-group/delete/delete_test.go index ec7143cbf..9383a7dbc 100644 --- a/internal/cmd/security-group/delete/delete_test.go +++ b/internal/cmd/security-group/delete/delete_test.go @@ -4,30 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testGroupId = uuid.NewString() ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -37,7 +40,11 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, SecurityGroupId: testGroupId, } for _, mod := range mods { @@ -47,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRequest)) iaas.ApiDeleteSecurityGroupRequest { - request := testClient.DeleteSecurityGroup(testCtx, testProjectId, testGroupId) + request := testClient.DefaultAPI.DeleteSecurityGroup(testCtx, testProjectId, testRegion, testGroupId) for _, mod := range mods { mod(&request) } @@ -57,6 +64,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRequest)) i func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string args []string isValid bool @@ -72,14 +80,14 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -105,8 +113,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -137,7 +145,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -174,7 +182,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/security-group/describe/describe.go b/internal/cmd/security-group/describe/describe.go index 1ea5bf64d..9f9a9591c 100644 --- a/internal/cmd/security-group/describe/describe.go +++ b/internal/cmd/security-group/describe/describe.go @@ -2,11 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,9 +16,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type inputModel struct { @@ -28,7 +27,7 @@ type inputModel struct { const groupIdArg = "GROUP_ID" -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", groupIdArg), Short: "Describes security groups", @@ -70,7 +69,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetSecurityGroupRequest { - request := apiClient.GetSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId) + request := apiClient.DefaultAPI.GetSecurityGroup(ctx, model.ProjectId, model.Region, model.SecurityGroupId) return request } @@ -85,15 +84,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM SecurityGroupId: cliArgs[0], } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -101,24 +92,7 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou if resp == nil { return fmt.Errorf("security group response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal security group: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal security group: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { var content []tables.Table table := tables.NewTable() @@ -129,10 +103,8 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou } table.AddSeparator() - if name := resp.Name; name != nil { - table.AddRow("NAME", *name) - table.AddSeparator() - } + table.AddRow("NAME", resp.Name) + table.AddSeparator() if description := resp.Description; description != nil { table.AddRow("DESCRIPTION", *description) @@ -144,9 +116,9 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou table.AddSeparator() } - if resp.Labels != nil && len(*resp.Labels) > 0 { + if len(resp.Labels) > 0 { var labels []string - for key, value := range *resp.Labels { + for key, value := range resp.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -165,7 +137,7 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou content = append(content, table) - if resp.Rules != nil && len(*resp.Rules) > 0 { + if len(resp.Rules) > 0 { rulesTable := tables.NewTable() rulesTable.SetTitle("RULES") rulesTable.SetHeader( @@ -180,10 +152,10 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou "REMOTE SECURITY GROUP ID", ) - for _, rule := range *resp.Rules { + for _, rule := range resp.Rules { var portRange string if rule.PortRange != nil { - portRange = fmt.Sprintf("%s-%s", utils.PtrString(rule.PortRange.Min), utils.PtrString(rule.PortRange.Max)) + portRange = fmt.Sprintf("%d-%d", rule.PortRange.Min, rule.PortRange.Max) } var protocol string @@ -193,14 +165,14 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou var icmpParameter string if rule.IcmpParameters != nil { - icmpParameter = fmt.Sprintf("type: %s, code: %s", utils.PtrString(rule.IcmpParameters.Type), utils.PtrString(rule.IcmpParameters.Code)) + icmpParameter = fmt.Sprintf("type: %d, code: %d", rule.IcmpParameters.Type, rule.IcmpParameters.Code) } rulesTable.AddRow( utils.PtrString(rule.Id), utils.PtrString(rule.Description), protocol, - utils.PtrString(rule.Direction), + rule.Direction, utils.PtrString(rule.Ethertype), portRange, utils.PtrString(rule.IpRange), @@ -217,5 +189,5 @@ func outputResult(p *print.Printer, outputFormat string, resp *iaas.SecurityGrou } return nil - } + }) } diff --git a/internal/cmd/security-group/describe/describe_test.go b/internal/cmd/security-group/describe/describe_test.go index f9b5f7fba..d0328fd67 100644 --- a/internal/cmd/security-group/describe/describe_test.go +++ b/internal/cmd/security-group/describe/describe_test.go @@ -7,26 +7,30 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testSecurityGroupId = []string{uuid.NewString()} ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -36,7 +40,11 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, SecurityGroupId: testSecurityGroupId[0], } for _, mod := range mods { @@ -46,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRequest)) iaas.ApiGetSecurityGroupRequest { - request := testClient.GetSecurityGroup(testCtx, testProjectId, testSecurityGroupId[0]) + request := testClient.DefaultAPI.GetSecurityGroup(testCtx, testProjectId, testRegion, testSecurityGroupId[0]) for _, mod := range mods { mod(&request) } @@ -56,6 +64,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRequest)) iaas func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool args []string @@ -77,7 +86,7 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), args: testSecurityGroupId, isValid: false, @@ -85,7 +94,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), args: testSecurityGroupId, isValid: false, @@ -93,7 +102,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), args: testSecurityGroupId, isValid: false, @@ -119,8 +128,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) if err := globalflags.Configure(cmd.Flags()); err != nil { t.Errorf("cannot configure global flags: %v", err) } @@ -147,7 +156,7 @@ func TestParseInput(t *testing.T) { } } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -184,7 +193,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -216,11 +225,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/security-group/list/list.go b/internal/cmd/security-group/list/list.go index 1459a3c11..95c83a7d5 100644 --- a/internal/cmd/security-group/list/list.go +++ b/internal/cmd/security-group/list/list.go @@ -2,13 +2,14 @@ package list import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,31 +20,40 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { *globalflags.GlobalFlagModel LabelSelector *string + Limit *int64 } const ( labelSelectorFlag = "label-selector" + limitFlag = "limit" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists security groups", Long: "Lists security groups by its internal ID.", Args: args.NoArgs, Example: examples.Build( - examples.NewExample(`List all groups`, `$ stackit security-group list`), - examples.NewExample(`List groups with labels`, `$ stackit security-group list --label-selector label1=value1,label2=value2`), + examples.NewExample(`Lists all security groups`, `$ stackit security-group list`), + examples.NewExample(`Lists security groups with labels`, `$ stackit security-group list --label-selector label1=value1,label2=value2`), + examples.NewExample( + `Lists all security groups in JSON format`, + "$ stackit security-group list --output-format json", + ), + examples.NewExample( + `Lists up to 10 security groups`, + "$ stackit security-group list --limit 10", + ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -54,12 +64,6 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - // Call API request := buildRequest(ctx, model, apiClient) @@ -68,15 +72,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list security group: %w", err) } - if items := response.GetItems(); len(items) == 0 { - params.Printer.Info("No security groups found for project %q", projectLabel) - } else { - if err := outputResult(params.Printer, model.OutputFormat, items); err != nil { - return fmt.Errorf("output security groups: %w", err) - } + items := response.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - return nil + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } @@ -86,65 +95,54 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + model := inputModel{ GlobalFlagModel: globalFlags, LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListSecurityGroupsRequest { - request := apiClient.ListSecurityGroups(ctx, model.ProjectId) + request := apiClient.DefaultAPI.ListSecurityGroups(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { request = request.LabelSelector(*model.LabelSelector) } return request } -func outputResult(p *print.Printer, outputFormat string, items []iaas.SecurityGroup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(items, "", " ") - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(items, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []iaas.SecurityGroup) error { + return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No security groups found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATEFUL", "DESCRIPTION", "LABELS") for _, item := range items { var labelsString string if item.Labels != nil { var labels []string - for key, value := range *item.Labels { + for key, value := range item.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } labelsString = strings.Join(labels, ", ") @@ -152,7 +150,7 @@ func outputResult(p *print.Printer, outputFormat string, items []iaas.SecurityGr table.AddRow( utils.PtrString(item.Id), - utils.PtrString(item.Name), + item.Name, utils.PtrString(item.Stateful), utils.PtrString(item.Description), labelsString, @@ -164,5 +162,5 @@ func outputResult(p *print.Printer, outputFormat string, items []iaas.SecurityGr } return nil - } + }) } diff --git a/internal/cmd/security-group/list/list_test.go b/internal/cmd/security-group/list/list_test.go index bade91812..6e77eec9b 100644 --- a/internal/cmd/security-group/list/list_test.go +++ b/internal/cmd/security-group/list/list_test.go @@ -7,28 +7,34 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testLabels = "fooKey=fooValue,barKey=barValue,bazKey=bazValue" ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + labelSelectorFlag: testLabels, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -38,8 +44,13 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - LabelSelector: utils.Ptr(testLabels), + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + LabelSelector: utils.Ptr(testLabels), + Limit: utils.Ptr(int64(10)), } for _, mod := range mods { mod(model) @@ -48,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupsRequest)) iaas.ApiListSecurityGroupsRequest { - request := testClient.ListSecurityGroups(testCtx, testProjectId) + request := testClient.DefaultAPI.ListSecurityGroups(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabels) for _, mod := range mods { mod(&request) @@ -59,6 +70,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupsRequest)) ia func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -77,21 +89,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -115,48 +127,25 @@ func TestParseInput(t *testing.T) { model.LabelSelector = utils.Ptr("foo=bar") }), }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - if err := globalflags.Configure(cmd.Flags()); err != nil { - t.Errorf("cannot configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - if err := cmd.ValidateRequiredFlags(); err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -178,7 +167,7 @@ func TestBuildRequest(t *testing.T) { model.LabelSelector = utils.Ptr("") }), expectedRequest: fixtureRequest(func(request *iaas.ApiListSecurityGroupsRequest) { - *request = (*request).LabelSelector("") + *request = request.LabelSelector("") }), }, { @@ -187,7 +176,7 @@ func TestBuildRequest(t *testing.T) { model.LabelSelector = utils.Ptr("foo=bar") }), expectedRequest: fixtureRequest(func(request *iaas.ApiListSecurityGroupsRequest) { - *request = (*request).LabelSelector("foo=bar") + *request = request.LabelSelector("foo=bar") }), }, } @@ -197,7 +186,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -209,6 +198,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []iaas.SecurityGroup } tests := []struct { @@ -222,11 +212,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.items); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/security-group/rule/create/create.go b/internal/cmd/security-group/rule/create/create.go index d0e7bb008..a5248580c 100644 --- a/internal/cmd/security-group/rule/create/create.go +++ b/internal/cmd/security-group/rule/create/create.go @@ -2,11 +2,12 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -40,7 +40,7 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel SecurityGroupId string - Direction *string + Direction string Description *string EtherType *string IcmpParameterCode *int64 @@ -53,7 +53,7 @@ type inputModel struct { ProtocolName *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a security group rule", @@ -77,9 +77,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit security-group rule create --security-group-id xxx --direction ingress --protocol-number 1`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -96,18 +96,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId) + securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) securityGroupLabel = model.SecurityGroupId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a security group rule for security group %q for project %q?", securityGroupLabel, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a security group rule for security group %q for project %q?", securityGroupLabel, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -143,7 +141,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -152,7 +150,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), - Direction: flags.FlagToStringPointer(p, cmd, directionFlag), + Direction: flags.FlagToStringValue(p, cmd, directionFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), EtherType: flags.FlagToStringPointer(p, cmd, etherTypeFlag), IcmpParameterCode: flags.FlagToInt64Pointer(p, cmd, icmpParameterCodeFlag), @@ -165,20 +163,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ProtocolName: flags.FlagToStringPointer(p, cmd, protocolNameFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateSecurityGroupRuleRequest { - req := apiClient.CreateSecurityGroupRule(ctx, model.ProjectId, model.SecurityGroupId) + req := apiClient.DefaultAPI.CreateSecurityGroupRule(ctx, model.ProjectId, model.Region, model.SecurityGroupId) icmpParameters := &iaas.ICMPParameters{} portRange := &iaas.PortRange{} protocol := &iaas.CreateProtocol{} @@ -192,15 +182,15 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } if model.IcmpParameterCode != nil || model.IcmpParameterType != nil { - icmpParameters.Code = model.IcmpParameterCode - icmpParameters.Type = model.IcmpParameterType + icmpParameters.Code = *model.IcmpParameterCode + icmpParameters.Type = *model.IcmpParameterType payload.IcmpParameters = icmpParameters } if model.PortRangeMax != nil || model.PortRangeMin != nil { - portRange.Max = model.PortRangeMax - portRange.Min = model.PortRangeMin + portRange.Max = *model.PortRangeMax + portRange.Min = *model.PortRangeMin payload.PortRange = portRange } @@ -223,29 +213,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, securityGro if securityGroupRule == nil { return fmt.Errorf("security group rule is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(securityGroupRule, "", " ") - if err != nil { - return fmt.Errorf("marshal security group rule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(securityGroupRule, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal security group rule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, securityGroupRule, func() error { operationState := "Created" if model.Async { operationState = "Triggered creation of" } p.Outputf("%s security group rule for security group %q in project %q.\nSecurity group rule ID: %s\n", operationState, securityGroupName, projectLabel, utils.PtrString(securityGroupRule.Id)) return nil - } + }) } diff --git a/internal/cmd/security-group/rule/create/create_test.go b/internal/cmd/security-group/rule/create/create_test.go index 6c1521899..de81f8ca9 100644 --- a/internal/cmd/security-group/rule/create/create_test.go +++ b/internal/cmd/security-group/rule/create/create_test.go @@ -4,22 +4,26 @@ import ( "context" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testSecurityGroupId = uuid.NewString() @@ -27,7 +31,9 @@ var testRemoteSecurityGroupId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + securityGroupIdFlag: testSecurityGroupId, directionFlag: "ingress", descriptionFlag: "example-description", @@ -51,10 +57,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, SecurityGroupId: testSecurityGroupId, - Direction: utils.Ptr("ingress"), + Direction: "ingress", Description: utils.Ptr("example-description"), EtherType: utils.Ptr("ether"), IcmpParameterCode: utils.Ptr(int64(0)), @@ -73,7 +80,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest)) iaas.ApiCreateSecurityGroupRuleRequest { - request := testClient.CreateSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId) + request := testClient.DefaultAPI.CreateSecurityGroupRule(testCtx, testProjectId, testRegion, testSecurityGroupId) request = request.CreateSecurityGroupRulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -82,9 +89,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest } func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRuleRequest)) iaas.ApiCreateSecurityGroupRuleRequest { - request := testClient.CreateSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId) + request := testClient.DefaultAPI.CreateSecurityGroupRule(testCtx, testProjectId, testRegion, testSecurityGroupId) request = request.CreateSecurityGroupRulePayload(iaas.CreateSecurityGroupRulePayload{ - Direction: utils.Ptr("ingress"), + Direction: "ingress", }) for _, mod := range mods { mod(&request) @@ -94,17 +101,17 @@ func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateSecurityGroupRul func fixturePayload(mods ...func(payload *iaas.CreateSecurityGroupRulePayload)) iaas.CreateSecurityGroupRulePayload { payload := iaas.CreateSecurityGroupRulePayload{ - Direction: utils.Ptr("ingress"), + Direction: "ingress", Description: utils.Ptr("example-description"), Ethertype: utils.Ptr("ether"), IcmpParameters: &iaas.ICMPParameters{ - Code: utils.Ptr(int64(0)), - Type: utils.Ptr(int64(8)), + Code: int64(0), + Type: int64(8), }, IpRange: utils.Ptr("10.1.2.3"), PortRange: &iaas.PortRange{ - Max: utils.Ptr(int64(24)), - Min: utils.Ptr(int64(22)), + Max: int64(24), + Min: int64(22), }, Protocol: &iaas.CreateProtocol{ Int64: utils.Ptr(int64(1)), @@ -121,6 +128,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateSecurityGroupRulePayload)) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -200,21 +208,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -243,53 +251,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flag groups: %v", err) - } - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -310,9 +272,10 @@ func TestBuildRequest(t *testing.T) { model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - Direction: utils.Ptr("ingress"), + Direction: "ingress", SecurityGroupId: testSecurityGroupId, }, expectedRequest: fixtureRequiredRequest(), @@ -324,7 +287,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), cmp.AllowUnexported(iaas.NullableString{}), ) if diff != "" { @@ -360,11 +323,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.securityGroupName, tt.args.securityGroupRule); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.securityGroupName, tt.args.securityGroupRule); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/security-group/rule/delete/delete.go b/internal/cmd/security-group/rule/delete/delete.go index 6acae57c7..916acd6ac 100644 --- a/internal/cmd/security-group/rule/delete/delete.go +++ b/internal/cmd/security-group/rule/delete/delete.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -27,10 +29,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel SecurityGroupRuleId string - SecurityGroupId *string + SecurityGroupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", securityGroupRuleIdArg), Short: "Deletes a security group rule", @@ -58,24 +60,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, *model.SecurityGroupId) + securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) - securityGroupLabel = *model.SecurityGroupId + securityGroupLabel = model.SecurityGroupId } - securityGroupRuleLabel, err := iaasUtils.GetSecurityGroupRuleName(ctx, apiClient, model.ProjectId, model.SecurityGroupRuleId, *model.SecurityGroupId) + securityGroupRuleLabel, err := iaasUtils.GetSecurityGroupRuleName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupRuleId, model.SecurityGroupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get security group rule name: %v", err) securityGroupRuleLabel = model.SecurityGroupRuleId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete security group rule %q from security group %q?", securityGroupRuleLabel, securityGroupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete security group rule %q from security group %q?", securityGroupRuleLabel, securityGroupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,21 +111,13 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, SecurityGroupRuleId: securityGroupRuleId, - SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteSecurityGroupRuleRequest { - return apiClient.DeleteSecurityGroupRule(ctx, model.ProjectId, *model.SecurityGroupId, model.SecurityGroupRuleId) + return apiClient.DefaultAPI.DeleteSecurityGroupRule(ctx, model.ProjectId, model.Region, model.SecurityGroupId, model.SecurityGroupRuleId) } diff --git a/internal/cmd/security-group/rule/delete/delete_test.go b/internal/cmd/security-group/rule/delete/delete_test.go index ebf83035c..c162029b7 100644 --- a/internal/cmd/security-group/rule/delete/delete_test.go +++ b/internal/cmd/security-group/rule/delete/delete_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testSecurityGroupId = uuid.NewString() @@ -38,7 +39,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + securityGroupIdFlag: testSecurityGroupId, } for _, mod := range mods { @@ -51,9 +54,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - SecurityGroupId: utils.Ptr(testSecurityGroupId), + SecurityGroupId: testSecurityGroupId, SecurityGroupRuleId: testSecurityGroupRuleId, } for _, mod := range mods { @@ -63,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteSecurityGroupRuleRequest)) iaas.ApiDeleteSecurityGroupRuleRequest { - request := testClient.DeleteSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId, testSecurityGroupRuleId) + request := testClient.DefaultAPI.DeleteSecurityGroupRule(testCtx, testProjectId, testRegion, testSecurityGroupId, testSecurityGroupRuleId) for _, mod := range mods { mod(&request) } @@ -95,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -103,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -111,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -155,8 +159,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -188,7 +192,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -226,7 +230,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/security-group/rule/describe/describe.go b/internal/cmd/security-group/rule/describe/describe.go index 47cd0e554..c6b2c9ea5 100644 --- a/internal/cmd/security-group/rule/describe/describe.go +++ b/internal/cmd/security-group/rule/describe/describe.go @@ -2,11 +2,12 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -30,10 +30,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel SecurityGroupRuleId string - SecurityGroupId *string + SecurityGroupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", securityGroupRuleIdArg), Short: "Shows details of a security group rule", @@ -94,47 +94,22 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, SecurityGroupRuleId: securityGroupRuleId, - SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetSecurityGroupRuleRequest { - return apiClient.GetSecurityGroupRule(ctx, model.ProjectId, *model.SecurityGroupId, model.SecurityGroupRuleId) + return apiClient.DefaultAPI.GetSecurityGroupRule(ctx, model.ProjectId, model.Region, model.SecurityGroupId, model.SecurityGroupRuleId) } func outputResult(p *print.Printer, outputFormat string, securityGroupRule *iaas.SecurityGroupRule) error { if securityGroupRule == nil { return fmt.Errorf("security group rule is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(securityGroupRule, "", " ") - if err != nil { - return fmt.Errorf("marshal security group rule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(securityGroupRule, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal security group rule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, securityGroupRule, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(securityGroupRule.Id)) table.AddSeparator() @@ -151,19 +126,14 @@ func outputResult(p *print.Printer, outputFormat string, securityGroupRule *iaas } } - table.AddRow("DIRECTION", utils.PtrString(securityGroupRule.Direction)) + table.AddRow("DIRECTION", securityGroupRule.Direction) table.AddSeparator() if securityGroupRule.PortRange != nil { - if securityGroupRule.PortRange.Min != nil { - table.AddRow("START PORT", *securityGroupRule.PortRange.Min) - table.AddSeparator() - } - - if securityGroupRule.PortRange.Max != nil { - table.AddRow("END PORT", *securityGroupRule.PortRange.Max) - table.AddSeparator() - } + table.AddRow("START PORT", securityGroupRule.PortRange.Min) + table.AddSeparator() + table.AddRow("END PORT", securityGroupRule.PortRange.Max) + table.AddSeparator() } if securityGroupRule.Ethertype != nil { @@ -186,5 +156,5 @@ func outputResult(p *print.Printer, outputFormat string, securityGroupRule *iaas return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/security-group/rule/describe/describe_test.go b/internal/cmd/security-group/rule/describe/describe_test.go index 66b536f5c..445338cb6 100644 --- a/internal/cmd/security-group/rule/describe/describe_test.go +++ b/internal/cmd/security-group/rule/describe/describe_test.go @@ -7,19 +7,21 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testSecurityGroupId = uuid.NewString() var testSecurityGroupRuleId = uuid.NewString() @@ -36,7 +38,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + securityGroupIdFlag: testSecurityGroupId, } for _, mod := range mods { @@ -49,9 +53,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - SecurityGroupId: utils.Ptr(testSecurityGroupId), + SecurityGroupId: testSecurityGroupId, SecurityGroupRuleId: testSecurityGroupRuleId, } for _, mod := range mods { @@ -61,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetSecurityGroupRuleRequest)) iaas.ApiGetSecurityGroupRuleRequest { - request := testClient.GetSecurityGroupRule(testCtx, testProjectId, testSecurityGroupId, testSecurityGroupRuleId) + request := testClient.DefaultAPI.GetSecurityGroupRule(testCtx, testProjectId, testRegion, testSecurityGroupId, testSecurityGroupRuleId) for _, mod := range mods { mod(&request) } @@ -105,7 +110,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +118,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +126,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -165,54 +170,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +194,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -268,11 +226,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.securityGroupRule); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.securityGroupRule); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/security-group/rule/list/list.go b/internal/cmd/security-group/rule/list/list.go index c0bec27c6..f357d01c3 100644 --- a/internal/cmd/security-group/rule/list/list.go +++ b/internal/cmd/security-group/rule/list/list.go @@ -2,10 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,9 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -32,10 +31,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel Limit *int64 - SecurityGroupId *string + SecurityGroupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all security group rules in a security group of a project", @@ -55,9 +54,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit security-group rule list --security-group-id xxx --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -75,29 +74,26 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list security group rules: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, *model.SecurityGroupId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) - securityGroupLabel = *model.SecurityGroupId - } + items := resp.GetItems() - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No rules found in security group %q for project %q\n", securityGroupLabel, projectLabel) - return nil + securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) + securityGroupLabel = model.SecurityGroupId + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, securityGroupLabel, items) }, } configureFlags(cmd) @@ -112,7 +108,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -129,44 +125,23 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, - SecurityGroupId: flags.FlagToStringPointer(p, cmd, securityGroupIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListSecurityGroupRulesRequest { - return apiClient.ListSecurityGroupRules(ctx, model.ProjectId, *model.SecurityGroupId) + return apiClient.DefaultAPI.ListSecurityGroupRules(ctx, model.ProjectId, model.Region, model.SecurityGroupId) } -func outputResult(p *print.Printer, outputFormat string, securityGroupRules []iaas.SecurityGroupRule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(securityGroupRules, "", " ") - if err != nil { - return fmt.Errorf("marshal security group rules: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(securityGroupRules, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal security group rules: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel, securityGroupLabel string, securityGroupRules []iaas.SecurityGroupRule) error { + return p.OutputResult(outputFormat, securityGroupRules, func() error { + if len(securityGroupRules) == 0 { + p.Outputf("No rules found in security group %q for project %q\n", securityGroupLabel, projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "ETHER TYPE", "DIRECTION", "PROTOCOL", "REMOTE SECURITY GROUP ID") @@ -183,7 +158,7 @@ func outputResult(p *print.Printer, outputFormat string, securityGroupRules []ia table.AddRow( utils.PtrString(securityGroupRule.Id), etherType, - utils.PtrString(securityGroupRule.Direction), + securityGroupRule.Direction, protocolName, utils.PtrString(securityGroupRule.RemoteSecurityGroupId), ) @@ -192,5 +167,5 @@ func outputResult(p *print.Printer, outputFormat string, securityGroupRules []ia p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/security-group/rule/list/list_test.go b/internal/cmd/security-group/rule/list/list_test.go index 0b7a49b25..1f3c7de50 100644 --- a/internal/cmd/security-group/rule/list/list_test.go +++ b/internal/cmd/security-group/rule/list/list_test.go @@ -7,25 +7,30 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testSecurityGroupId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", securityGroupIdFlag: testSecurityGroupId, } @@ -40,9 +45,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), - SecurityGroupId: utils.Ptr(testSecurityGroupId), + SecurityGroupId: testSecurityGroupId, } for _, mod := range mods { mod(model) @@ -51,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupRulesRequest)) iaas.ApiListSecurityGroupRulesRequest { - request := testClient.ListSecurityGroupRules(testCtx, testProjectId, testSecurityGroupId) + request := testClient.DefaultAPI.ListSecurityGroupRules(testCtx, testProjectId, testRegion, testSecurityGroupId) for _, mod := range mods { mod(&request) } @@ -61,6 +67,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListSecurityGroupRulesRequest) func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -84,21 +91,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -141,46 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +172,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -216,6 +184,8 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string + securityGroupLabel string securityGroupRules []iaas.SecurityGroupRule } tests := []struct { @@ -229,11 +199,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.securityGroupRules); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.securityGroupLabel, tt.args.securityGroupRules); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/security-group/rule/security_group_rule.go b/internal/cmd/security-group/rule/security_group_rule.go index 75f394384..fda58dd87 100644 --- a/internal/cmd/security-group/rule/security_group_rule.go +++ b/internal/cmd/security-group/rule/security_group_rule.go @@ -1,18 +1,18 @@ package rule import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/rule/create" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/rule/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/rule/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/rule/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "rule", Short: "Provides functionality for security group rules", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/security-group/security_group.go b/internal/cmd/security-group/security_group.go index 952da976c..ef613d054 100644 --- a/internal/cmd/security-group/security_group.go +++ b/internal/cmd/security-group/security_group.go @@ -1,7 +1,6 @@ package security_group import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/create" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/describe" @@ -9,13 +8,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/rule" "github.com/stackitcloud/stackit-cli/internal/cmd/security-group/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "security-group", Short: "Manage security groups", @@ -27,7 +27,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand( rule.NewCmd(params), create.NewCmd(params), diff --git a/internal/cmd/security-group/update/update.go b/internal/cmd/security-group/update/update.go index 487dca9e0..c62db431f 100644 --- a/internal/cmd/security-group/update/update.go +++ b/internal/cmd/security-group/update/update.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,12 +19,11 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type inputModel struct { *globalflags.GlobalFlagModel - Labels *map[string]string + Labels map[string]any Description *string Name *string SecurityGroupId string @@ -35,7 +37,7 @@ const ( labelsArg = "labels" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", groupNameArg), Short: "Updates a security group", @@ -64,18 +66,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - groupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.SecurityGroupId) + groupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) if err != nil { params.Printer.Warn("cannot retrieve groupname: %v", err) groupLabel = model.SecurityGroupId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update the security group %q?", groupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update the security group %q?", groupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -85,7 +85,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("update security group: %w", err) } - params.Printer.Info("Updated security group \"%v\" for %q\n", utils.PtrString(resp.Name), projectLabel) + params.Printer.Info("Updated security group \"%v\" for %q\n", resp.Name, projectLabel) return nil }, @@ -109,7 +109,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM model := inputModel{ GlobalFlagModel: globalFlags, - Labels: flags.FlagToStringToStringPointer(p, cmd, labelsArg), + Labels: flags.FlagToStringToAny(p, cmd, labelsArg), Description: flags.FlagToStringPointer(p, cmd, descriptionArg), Name: flags.FlagToStringPointer(p, cmd, nameArg), SecurityGroupId: cliArgs[0], @@ -119,23 +119,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, cliArgs []string) (*inputM return nil, fmt.Errorf("no flags have been passed") } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateSecurityGroupRequest { - request := apiClient.UpdateSecurityGroup(ctx, model.ProjectId, model.SecurityGroupId) + request := apiClient.DefaultAPI.UpdateSecurityGroup(ctx, model.ProjectId, model.Region, model.SecurityGroupId) payload := iaas.NewUpdateSecurityGroupPayload() payload.Description = model.Description - payload.Labels = utils.ConvertStringMapToInterfaceMap(model.Labels) + payload.Labels = model.Labels payload.Name = model.Name request = request.UpdateSecurityGroupPayload(*payload) diff --git a/internal/cmd/security-group/update/update_test.go b/internal/cmd/security-group/update/update_test.go index 1d89a9e48..1b5324968 100644 --- a/internal/cmd/security-group/update/update_test.go +++ b/internal/cmd/security-group/update/update_test.go @@ -4,49 +4,40 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testGroupId = []string{uuid.NewString()} testName = "new-security-group" testDescription = "a test description" - testLabels = map[string]string{ + testLabels = map[string]any{ "fooKey": "fooValue", "barKey": "barValue", "bazKey": "bazValue", } ) -func toStringAnyMapPtr(m map[string]string) map[string]any { - if m == nil { - return nil - } - result := map[string]any{} - for k, v := range m { - result[k] = v - } - return result -} - func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + descriptionArg: testDescription, labelsArg: "fooKey=fooValue,barKey=barValue,bazKey=bazValue", nameArg: testName, @@ -59,8 +50,12 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault}, - Labels: &testLabels, + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Labels: testLabels, Description: &testDescription, Name: &testName, SecurityGroupId: testGroupId[0], @@ -72,10 +67,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateSecurityGroupRequest)) iaas.ApiUpdateSecurityGroupRequest { - request := testClient.UpdateSecurityGroup(testCtx, testProjectId, testGroupId[0]) + request := testClient.DefaultAPI.UpdateSecurityGroup(testCtx, testProjectId, testRegion, testGroupId[0]) request = request.UpdateSecurityGroupPayload(iaas.UpdateSecurityGroupPayload{ Description: &testDescription, - Labels: utils.Ptr(toStringAnyMapPtr(testLabels)), + Labels: testLabels, Name: &testName, }) for _, mod := range mods { @@ -87,6 +82,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdateSecurityGroupRequest)) i func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string args []string isValid bool @@ -102,7 +98,7 @@ func TestParseInput(t *testing.T) { { description: "no values but valid group id", flagValues: map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, }, args: testGroupId, isValid: false, @@ -115,7 +111,7 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), args: testGroupId, isValid: false, @@ -123,7 +119,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), args: testGroupId, isValid: false, @@ -131,7 +127,7 @@ func TestParseInput(t *testing.T) { { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), args: testGroupId, isValid: false, @@ -177,7 +173,7 @@ func TestParseInput(t *testing.T) { args: testGroupId, isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ + model.Labels = map[string]any{ "foo": "bar", } }), @@ -204,8 +200,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) if err := globalflags.Configure(cmd.Flags()); err != nil { t.Errorf("cannot configure global flags: %v", err) } @@ -232,7 +228,7 @@ func TestParseInput(t *testing.T) { } } - model, err := parseInput(p, cmd, tt.args) + model, err := parseInput(params.Printer, cmd, tt.args) if err != nil { if !tt.isValid { return @@ -268,7 +264,7 @@ func TestBuildRequest(t *testing.T) { model.Labels = nil }), expectedRequest: fixtureRequest(func(request *iaas.ApiUpdateSecurityGroupRequest) { - *request = (*request).UpdateSecurityGroupPayload(iaas.UpdateSecurityGroupPayload{ + *request = request.UpdateSecurityGroupPayload(iaas.UpdateSecurityGroupPayload{ Description: &testDescription, Labels: nil, Name: &testName, @@ -282,7 +278,7 @@ func TestBuildRequest(t *testing.T) { request := buildRequest(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/backup.go b/internal/cmd/server/backup/backup.go index e29887143..77d290a42 100644 --- a/internal/cmd/server/backup/backup.go +++ b/internal/cmd/server/backup/backup.go @@ -1,7 +1,6 @@ package backup import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/create" del "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/describe" @@ -12,12 +11,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/schedule" volumebackup "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/volume-backup" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "backup", Short: "Provides functionality for server backups", @@ -29,7 +29,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(enable.NewCmd(params)) cmd.AddCommand(disable.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/server/backup/create/create.go b/internal/cmd/server/backup/create/create.go index ccabadb65..cd968652a 100644 --- a/internal/cmd/server/backup/create/create.go +++ b/internal/cmd/server/backup/create/create.go @@ -2,13 +2,15 @@ package create import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/spf13/cobra" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,10 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) const ( @@ -37,11 +35,11 @@ type inputModel struct { ServerId string BackupName string - BackupRetentionPeriod int64 + BackupRetentionPeriod int32 BackupVolumeIds []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Server Backup.", @@ -55,10 +53,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a Server Backup with name "mybackup" and retention period of 5 days`, `$ stackit server backup create --server-id xxx --name=mybackup --retention-period=5`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,7 +70,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -80,12 +78,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a Backup for server %s?", model.ServerId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a Backup for server %s?", model.ServerId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -108,14 +104,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name") - cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") + cmd.Flags().Int32P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") cmd.Flags().VarP(flags.UUIDSliceFlag(), backupVolumeIdsFlag, "i", "Backup volume IDs, as comma separated UUID values.") err := flags.MarkFlagsRequired(cmd, serverIdFlag, backupNameFlag) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -124,29 +120,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), - BackupRetentionPeriod: flags.FlagWithDefaultToInt64Value(p, cmd, backupRetentionPeriodFlag), + BackupRetentionPeriod: flags.FlagWithDefaultToInt32Value(p, cmd, backupRetentionPeriodFlag), BackupName: flags.FlagToStringValue(p, cmd, backupNameFlag), BackupVolumeIds: flags.FlagToStringSliceValue(p, cmd, backupVolumeIdsFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) (serverbackup.ApiCreateBackupRequest, error) { - req := apiClient.CreateBackup(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.CreateBackup(ctx, model.ProjectId, model.ServerId, model.Region) payload := serverbackup.CreateBackupPayload{ - Name: &model.BackupName, - RetentionPeriod: &model.BackupRetentionPeriod, - VolumeIds: &model.BackupVolumeIds, + Name: model.BackupName, + RetentionPeriod: model.BackupRetentionPeriod, + VolumeIds: model.BackupVolumeIds, } if model.BackupVolumeIds == nil { payload.VolumeIds = nil @@ -156,25 +144,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbacku } func outputResult(p *print.Printer, outputFormat, serverLabel string, resp serverbackup.BackupJob) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal server backup: %w", err) - } - p.Outputln(string(details)) - + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Triggered creation of server backup for server %s. Backup ID: %s\n", serverLabel, resp.Id) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server backup: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Triggered creation of server backup for server %s. Backup ID: %s\n", serverLabel, utils.PtrString(resp.Id)) - return nil - } + }) } diff --git a/internal/cmd/server/backup/create/create_test.go b/internal/cmd/server/backup/create/create_test.go index 71a50ff6b..fdb363870 100644 --- a/internal/cmd/server/backup/create/create_test.go +++ b/internal/cmd/server/backup/create/create_test.go @@ -4,21 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -49,7 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { }, ServerId: testServerId, BackupName: "example-backup-name", - BackupRetentionPeriod: int64(14), + BackupRetentionPeriod: int32(14), BackupVolumeIds: []string{testBackupVolumeId}, } for _, mod := range mods { @@ -59,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiCreateBackupRequest)) serverbackup.ApiCreateBackupRequest { - request := testClient.CreateBackup(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.CreateBackup(testCtx, testProjectId, testServerId, testRegion) request = request.CreateBackupPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -69,9 +68,9 @@ func fixtureRequest(mods ...func(request *serverbackup.ApiCreateBackupRequest)) func fixturePayload(mods ...func(payload *serverbackup.CreateBackupPayload)) serverbackup.CreateBackupPayload { payload := serverbackup.CreateBackupPayload{ - Name: utils.Ptr("example-backup-name"), - RetentionPeriod: utils.Ptr(int64(14)), - VolumeIds: utils.Ptr([]string{testBackupVolumeId}), + Name: "example-backup-name", + RetentionPeriod: int32(14), + VolumeIds: []string{testBackupVolumeId}, } for _, mod := range mods { mod(&payload) @@ -82,6 +81,7 @@ func fixturePayload(mods ...func(payload *serverbackup.CreateBackupPayload)) ser func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -131,46 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -228,11 +189,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/delete/delete.go b/internal/cmd/server/backup/delete/delete.go index 5fed34276..ddca7091a 100644 --- a/internal/cmd/server/backup/delete/delete.go +++ b/internal/cmd/server/backup/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", backupIdArg), Short: "Deletes a Server Backup.", @@ -53,12 +54,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete server backup %q? (This cannot be undone)", model.BackupId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete server backup %q? (This cannot be undone)", model.BackupId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -97,19 +96,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiDeleteBackupRequest { - req := apiClient.DeleteBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId) + req := apiClient.DefaultAPI.DeleteBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId) return req } diff --git a/internal/cmd/server/backup/delete/delete_test.go b/internal/cmd/server/backup/delete/delete_test.go index d1de3562f..9e8a8796e 100644 --- a/internal/cmd/server/backup/delete/delete_test.go +++ b/internal/cmd/server/backup/delete/delete_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiDeleteBackupRequest)) serverbackup.ApiDeleteBackupRequest { - request := testClient.DeleteBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId) + request := testClient.DefaultAPI.DeleteBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId) for _, mod := range mods { mod(&request) } @@ -130,54 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -201,7 +153,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/describe/describe.go b/internal/cmd/server/backup/describe/describe.go index b44ec3fbe..4d95882ca 100644 --- a/internal/cmd/server/backup/describe/describe.go +++ b/internal/cmd/server/backup/describe/describe.go @@ -2,12 +2,11 @@ package describe import ( "context" - "encoding/json" "fmt" "strconv" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { BackupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", backupIdArg), Short: "Shows details of a Server Backup", @@ -66,7 +65,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read server backup: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -94,54 +93,33 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupId: backupId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiGetBackupRequest { - req := apiClient.GetBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId) + req := apiClient.DefaultAPI.GetBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId) return req } -func outputResult(p *print.Printer, outputFormat string, backup serverbackup.Backup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backup, "", " ") - if err != nil { - return fmt.Errorf("marshal server backup: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server backup: %w", err) +func outputResult(p *print.Printer, outputFormat string, backup *serverbackup.Backup) error { + return p.OutputResult(outputFormat, backup, func() error { + if backup == nil { + return fmt.Errorf("backup is nil") } - p.Outputln(string(details)) - return nil - default: table := tables.NewTable() - table.AddRow("ID", utils.PtrString(backup.Id)) + table.AddRow("ID", backup.Id) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(backup.Name)) + table.AddRow("NAME", backup.Name) table.AddSeparator() table.AddRow("SIZE (GB)", utils.PtrString(backup.Size)) table.AddSeparator() - table.AddRow("STATUS", utils.PtrString(backup.Status)) + table.AddRow("STATUS", backup.Status) table.AddSeparator() - table.AddRow("CREATED AT", utils.PtrString(backup.CreatedAt)) + table.AddRow("CREATED AT", backup.CreatedAt) table.AddSeparator() - table.AddRow("EXPIRES AT", utils.PtrString(backup.ExpireAt)) + table.AddRow("EXPIRES AT", backup.ExpireAt) table.AddSeparator() lastRestored := utils.PtrStringDefault(backup.LastRestoredAt, "") @@ -149,7 +127,7 @@ func outputResult(p *print.Printer, outputFormat string, backup serverbackup.Bac table.AddSeparator() volBackups := "" if backups := backup.VolumeBackups; backups != nil { - volBackups = strconv.Itoa(len(*backups)) + volBackups = strconv.Itoa(len(backups)) } table.AddRow("VOLUME BACKUPS", volBackups) table.AddSeparator() @@ -160,5 +138,5 @@ func outputResult(p *print.Printer, outputFormat string, backup serverbackup.Bac } return nil - } + }) } diff --git a/internal/cmd/server/backup/describe/describe_test.go b/internal/cmd/server/backup/describe/describe_test.go index c706734f1..c22cbbae2 100644 --- a/internal/cmd/server/backup/describe/describe_test.go +++ b/internal/cmd/server/backup/describe/describe_test.go @@ -4,20 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() @@ -62,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiGetBackupRequest)) serverbackup.ApiGetBackupRequest { - request := testClient.GetBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId) + request := testClient.DefaultAPI.GetBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId) for _, mod := range mods { mod(&request) } @@ -130,54 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,7 +157,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -215,7 +169,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string - backup serverbackup.Backup + backup *serverbackup.Backup } tests := []struct { name string @@ -223,24 +177,34 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, + name: "backup is nil", + args: args{ + outputFormat: print.PrettyOutputFormat, + backup: nil, + }, + wantErr: true, + }, + { + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + backup: &serverbackup.Backup{}, + }, wantErr: false, }, { name: "output format json", args: args{ outputFormat: print.JSONOutputFormat, - backup: serverbackup.Backup{}, + backup: &serverbackup.Backup{}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/disable/disable.go b/internal/cmd/server/backup/disable/disable.go index f456647ba..a0cc59306 100644 --- a/internal/cmd/server/backup/disable/disable.go +++ b/internal/cmd/server/backup/disable/disable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,7 @@ import ( serverbackupUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "disable", Short: "Disables Server Backup service", @@ -40,9 +41,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Disable Server Backup functionality for your server.`, "$ stackit server backup disable --server-id=zzz"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -56,7 +57,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -64,7 +65,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - canDisable, err := serverbackupUtils.CanDisableBackupService(ctx, apiClient, model.ProjectId, model.ServerId, model.Region) + canDisable, err := serverbackupUtils.CanDisableBackupService(ctx, apiClient.DefaultAPI, model.ProjectId, model.ServerId, model.Region) if err != nil { return err } @@ -73,12 +74,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return nil } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disable the backup service for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disable the backup service for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -103,7 +102,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -114,19 +113,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiDisableServiceResourceRequest { - req := apiClient.DisableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.DisableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region) return req } diff --git a/internal/cmd/server/backup/disable/disable_test.go b/internal/cmd/server/backup/disable/disable_test.go index 7b13233e1..fb6d540f7 100644 --- a/internal/cmd/server/backup/disable/disable_test.go +++ b/internal/cmd/server/backup/disable/disable_test.go @@ -5,19 +5,18 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testRegion = "eu01" @@ -26,6 +25,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -49,7 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiDisableServiceResourceRequest)) serverbackup.ApiDisableServiceResourceRequest { - request := testClient.DisableServiceResource(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.DisableServiceResource(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,17 +59,23 @@ func fixtureRequest(mods ...func(request *serverbackup.ApiDisableServiceResource func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel }{ { - description: "base", - flagValues: fixtureFlagValues(), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.ServerId = "" + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "server id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "" }), + isValid: false, }, { description: "no values", @@ -101,46 +107,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -164,7 +131,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/enable/enable.go b/internal/cmd/server/backup/enable/enable.go index c4a4402b6..ca0c162a8 100644 --- a/internal/cmd/server/backup/enable/enable.go +++ b/internal/cmd/server/backup/enable/enable.go @@ -5,7 +5,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "enable", Short: "Enables Server Backup service", @@ -40,9 +41,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Enable Server Backup functionality for your server`, "$ stackit server backup enable --server-id=zzz"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -56,7 +57,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -64,12 +65,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to enable the Server Backup service for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to enable the Server Backup service for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -96,7 +95,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -107,20 +106,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiEnableServiceResourceRequest { payload := serverbackup.EnableServiceResourcePayload{} - req := apiClient.EnableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region).EnableServiceResourcePayload(payload) + req := apiClient.DefaultAPI.EnableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region).EnableServiceResourcePayload(payload) return req } diff --git a/internal/cmd/server/backup/enable/enable_test.go b/internal/cmd/server/backup/enable/enable_test.go index b4cc1cb77..dcfedf333 100644 --- a/internal/cmd/server/backup/enable/enable_test.go +++ b/internal/cmd/server/backup/enable/enable_test.go @@ -5,19 +5,18 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testRegion = "eu01" @@ -26,6 +25,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -49,7 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiEnableServiceResourceRequest)) serverbackup.ApiEnableServiceResourceRequest { - request := testClient.EnableServiceResource(testCtx, testProjectId, testServerId, testRegion).EnableServiceResourcePayload(serverbackup.EnableServiceResourcePayload{}) + request := testClient.DefaultAPI.EnableServiceResource(testCtx, testProjectId, testServerId, testRegion).EnableServiceResourcePayload(serverbackup.EnableServiceResourcePayload{}) for _, mod := range mods { mod(&request) } @@ -59,17 +59,23 @@ func fixtureRequest(mods ...func(request *serverbackup.ApiEnableServiceResourceR func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel }{ { - description: "base", - flagValues: fixtureFlagValues(), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.ServerId = "" + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "server id is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "" }), + isValid: false, }, { description: "no values", @@ -101,46 +107,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -164,7 +131,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/list/list.go b/internal/cmd/server/backup/list/list.go index 775f88198..5d652a4d2 100644 --- a/internal/cmd/server/backup/list/list.go +++ b/internal/cmd/server/backup/list/list.go @@ -2,14 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,7 +21,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) const ( @@ -34,7 +34,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server backups", @@ -48,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all backups for a server with ID "xxx" in JSON format`, "$ stackit server backup list --server-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,27 +67,24 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server backups: %w", err) } - backups := *resp.Items - if len(backups) == 0 { - serverLabel := model.ServerId - // Get server name - if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - } else if serverName != "" { - serverLabel = serverName - } + backups := resp.GetItems() + + // Get server name + serverLabel := model.ServerId + if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) + } else if serverName != "" { + serverLabel = serverName } - params.Printer.Info("No backups found for server %s\n", serverLabel) - return nil } // Truncate output if model.Limit != nil && len(backups) > int(*model.Limit) { backups = backups[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, backups) + return outputResult(params.Printer, model.OutputFormat, serverLabel, backups) }, } configureFlags(cmd) @@ -102,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -122,60 +119,38 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiListBackupsRequest { - req := apiClient.ListBackups(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.ListBackups(ctx, model.ProjectId, model.ServerId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, backups []serverbackup.Backup) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backups, "", " ") - if err != nil { - return fmt.Errorf("marshal Server Backup list: %w", err) +func outputResult(p *print.Printer, outputFormat, serverLabel string, backups []serverbackup.Backup) error { + return p.OutputResult(outputFormat, backups, func() error { + if len(backups) == 0 { + p.Outputf("No backups found for server %s\n", serverLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Server Backup list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "SIZE (GB)", "STATUS", "CREATED AT", "EXPIRES AT", "LAST RESTORED AT", "VOLUME BACKUPS") - for i := range backups { - s := backups[i] - - lastRestored := utils.PtrStringDefault(s.LastRestoredAt, "") + for _, s := range backups { var volBackups int if s.VolumeBackups != nil { - volBackups = len(*s.VolumeBackups) + volBackups = len(s.VolumeBackups) } + table.AddRow( - utils.PtrString(s.Id), - utils.PtrString(s.Name), + s.Id, + s.Name, utils.PtrString(s.Size), - utils.PtrString(s.Status), - utils.PtrString(s.CreatedAt), - utils.PtrString(s.ExpireAt), - lastRestored, + s.Status, + s.CreatedAt, + s.ExpireAt, + utils.PtrString(s.LastRestoredAt), volBackups, ) } @@ -184,5 +159,5 @@ func outputResult(p *print.Printer, outputFormat string, backups []serverbackup. return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/backup/list/list_test.go b/internal/cmd/server/backup/list/list_test.go index 218a17c6e..a4fb74acc 100644 --- a/internal/cmd/server/backup/list/list_test.go +++ b/internal/cmd/server/backup/list/list_test.go @@ -4,21 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testRegion = "eu01" @@ -53,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiListBackupsRequest)) serverbackup.ApiListBackupsRequest { - request := testClient.ListBackups(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.ListBackups(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +63,7 @@ func fixtureRequest(mods ...func(request *serverbackup.ApiListBackupsRequest)) s func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -117,46 +118,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -180,7 +142,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -192,6 +154,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + serverLabel string backups []serverbackup.Backup } tests := []struct { @@ -214,11 +177,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backups); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.backups); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/restore/restore.go b/internal/cmd/server/backup/restore/restore.go index db9a48bac..5ba47b57b 100644 --- a/internal/cmd/server/backup/restore/restore.go +++ b/internal/cmd/server/backup/restore/restore.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -35,7 +36,7 @@ type inputModel struct { BackupVolumeIds []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("restore %s", backupIdArg), Short: "Restores a Server Backup.", @@ -62,12 +63,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to restore server backup %q? (This cannot be undone)", model.BackupId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to restore server backup %q? (This cannot be undone)", model.BackupId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -110,23 +109,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu StartServerAfterRestore: flags.FlagToBoolValue(p, cmd, startServerAfterRestoreFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiRestoreBackupRequest { - req := apiClient.RestoreBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId) + req := apiClient.DefaultAPI.RestoreBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId) payload := serverbackup.RestoreBackupPayload{ - StartServerAfterRestore: &model.StartServerAfterRestore, - VolumeIds: &model.BackupVolumeIds, + StartServerAfterRestore: model.StartServerAfterRestore, + VolumeIds: model.BackupVolumeIds, } if model.BackupVolumeIds == nil { payload.VolumeIds = nil diff --git a/internal/cmd/server/backup/restore/restore_test.go b/internal/cmd/server/backup/restore/restore_test.go index ff14455c2..ceef0fe37 100644 --- a/internal/cmd/server/backup/restore/restore_test.go +++ b/internal/cmd/server/backup/restore/restore_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() @@ -62,9 +61,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiRestoreBackupRequest)) serverbackup.ApiRestoreBackupRequest { - request := testClient.RestoreBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId) - startServerAfterRestore := false - request = request.RestoreBackupPayload(serverbackup.RestoreBackupPayload{StartServerAfterRestore: &startServerAfterRestore}) + request := testClient.DefaultAPI.RestoreBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId) + request = request.RestoreBackupPayload(serverbackup.RestoreBackupPayload{StartServerAfterRestore: false}) for _, mod := range mods { mod(&request) } @@ -132,54 +130,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -203,7 +154,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/schedule/create/create.go b/internal/cmd/server/backup/schedule/create/create.go index c400e4e06..4e57d4862 100644 --- a/internal/cmd/server/backup/schedule/create/create.go +++ b/internal/cmd/server/backup/schedule/create/create.go @@ -2,13 +2,15 @@ package create import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/spf13/cobra" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,10 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) const ( @@ -45,11 +43,11 @@ type inputModel struct { Enabled bool Rrule string BackupName string - BackupRetentionPeriod int64 + BackupRetentionPeriod int32 BackupVolumeIds []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Server Backup Schedule", @@ -63,10 +61,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a Server Backup Schedule with name "myschedule", backup name "mybackup" and retention period of 5 days`, `$ stackit server backup schedule create --server-id xxx --backup-name=mybackup --backup-schedule-name=myschedule --backup-retention-period=5`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -80,7 +78,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -88,12 +86,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a Backup Schedule for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a Backup Schedule for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -117,7 +113,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") cmd.Flags().StringP(backupScheduleNameFlag, "n", "", "Backup schedule name") cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name") - cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") + cmd.Flags().Int32P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server backup schedule enabled") cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "Backup RRULE (recurrence rule)") cmd.Flags().VarP(flags.UUIDSliceFlag(), backupVolumeIdsFlag, "i", "Backup volume IDs, as comma separated UUID values.") @@ -126,7 +122,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -135,7 +131,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), - BackupRetentionPeriod: flags.FlagWithDefaultToInt64Value(p, cmd, backupRetentionPeriodFlag), + BackupRetentionPeriod: flags.FlagWithDefaultToInt32Value(p, cmd, backupRetentionPeriodFlag), BackupScheduleName: flags.FlagToStringValue(p, cmd, backupScheduleNameFlag), BackupName: flags.FlagToStringValue(p, cmd, backupNameFlag), Rrule: flags.FlagWithDefaultToStringValue(p, cmd, rruleFlag), @@ -143,57 +139,32 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { BackupVolumeIds: flags.FlagToStringSliceValue(p, cmd, backupVolumeIdsFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) (serverbackup.ApiCreateBackupScheduleRequest, error) { - req := apiClient.CreateBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.CreateBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region) backupProperties := serverbackup.BackupProperties{ - Name: &model.BackupName, - RetentionPeriod: &model.BackupRetentionPeriod, - VolumeIds: &model.BackupVolumeIds, + Name: model.BackupName, + RetentionPeriod: model.BackupRetentionPeriod, + VolumeIds: model.BackupVolumeIds, } if model.BackupVolumeIds == nil { backupProperties.VolumeIds = nil } req = req.CreateBackupSchedulePayload(serverbackup.CreateBackupSchedulePayload{ - Enabled: &model.Enabled, - Name: &model.BackupScheduleName, - Rrule: &model.Rrule, + Enabled: model.Enabled, + Name: model.BackupScheduleName, + Rrule: model.Rrule, BackupProperties: &backupProperties, }) return req, nil } func outputResult(p *print.Printer, outputFormat, serverLabel string, resp serverbackup.BackupSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal server backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created server backup schedule for server %s. Backup Schedule ID: %s\n", serverLabel, utils.PtrString(resp.Id)) + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created server backup schedule for server %s. Backup Schedule ID: %d\n", serverLabel, resp.Id) return nil - } + }) } diff --git a/internal/cmd/server/backup/schedule/create/create_test.go b/internal/cmd/server/backup/schedule/create/create_test.go index ceb92188b..87c475a83 100644 --- a/internal/cmd/server/backup/schedule/create/create_test.go +++ b/internal/cmd/server/backup/schedule/create/create_test.go @@ -4,21 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -55,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Enabled: defaultEnabled, Rrule: defaultRrule, BackupName: "example-backup-name", - BackupRetentionPeriod: int64(14), + BackupRetentionPeriod: int32(14), BackupVolumeIds: []string{testVolumeId}, } for _, mod := range mods { @@ -65,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiCreateBackupScheduleRequest)) serverbackup.ApiCreateBackupScheduleRequest { - request := testClient.CreateBackupSchedule(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.CreateBackupSchedule(testCtx, testProjectId, testServerId, testRegion) request = request.CreateBackupSchedulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -75,13 +74,13 @@ func fixtureRequest(mods ...func(request *serverbackup.ApiCreateBackupScheduleRe func fixturePayload(mods ...func(payload *serverbackup.CreateBackupSchedulePayload)) serverbackup.CreateBackupSchedulePayload { payload := serverbackup.CreateBackupSchedulePayload{ - Name: utils.Ptr("example-backup-schedule-name"), - Enabled: utils.Ptr(defaultEnabled), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), + Name: "example-backup-schedule-name", + Enabled: defaultEnabled, + Rrule: "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1", BackupProperties: &serverbackup.BackupProperties{ - Name: utils.Ptr("example-backup-name"), - RetentionPeriod: utils.Ptr(int64(14)), - VolumeIds: utils.Ptr([]string{testVolumeId}), + Name: "example-backup-name", + RetentionPeriod: int32(14), + VolumeIds: []string{testVolumeId}, }, } for _, mod := range mods { @@ -93,6 +92,7 @@ func fixturePayload(mods ...func(payload *serverbackup.CreateBackupSchedulePaylo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -142,46 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -213,7 +174,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -238,11 +199,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/schedule/delete/delete.go b/internal/cmd/server/backup/schedule/delete/delete.go index fbf35a733..2a1250832 100644 --- a/internal/cmd/server/backup/schedule/delete/delete.go +++ b/internal/cmd/server/backup/schedule/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -30,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", scheduleIdArg), Short: "Deletes a Server Backup Schedule", @@ -57,7 +58,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -65,12 +66,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete server backup schedule %q? (This cannot be undone)", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete server backup schedule %q? (This cannot be undone)", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -109,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiDeleteBackupScheduleRequest { - req := apiClient.DeleteBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.ScheduleId) + req := apiClient.DefaultAPI.DeleteBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.ScheduleId) return req } diff --git a/internal/cmd/server/backup/schedule/delete/delete_test.go b/internal/cmd/server/backup/schedule/delete/delete_test.go index 26c218485..6d9420022 100644 --- a/internal/cmd/server/backup/schedule/delete/delete_test.go +++ b/internal/cmd/server/backup/schedule/delete/delete_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupScheduleId = "5" @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiDeleteBackupScheduleRequest)) serverbackup.ApiDeleteBackupScheduleRequest { - request := testClient.DeleteBackupSchedule(testCtx, testProjectId, testServerId, testRegion, testBackupScheduleId) + request := testClient.DefaultAPI.DeleteBackupSchedule(testCtx, testProjectId, testServerId, testRegion, testBackupScheduleId) for _, mod := range mods { mod(&request) } @@ -130,54 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -201,7 +153,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/schedule/describe/describe.go b/internal/cmd/server/backup/schedule/describe/describe.go index 49c5fd5ee..a696666d3 100644 --- a/internal/cmd/server/backup/schedule/describe/describe.go +++ b/internal/cmd/server/backup/schedule/describe/describe.go @@ -2,11 +2,10 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -32,7 +31,7 @@ type inputModel struct { BackupScheduleId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", backupScheduleIdArg), Short: "Shows details of a Server Backup Schedule", @@ -93,58 +92,33 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupScheduleId: backupScheduleId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiGetBackupScheduleRequest { - req := apiClient.GetBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupScheduleId) + req := apiClient.DefaultAPI.GetBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupScheduleId) return req } func outputResult(p *print.Printer, outputFormat string, schedule serverbackup.BackupSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(schedule, "", " ") - if err != nil { - return fmt.Errorf("marshal server backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(schedule, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, schedule, func() error { table := tables.NewTable() - table.AddRow("SCHEDULE ID", utils.PtrString(schedule.Id)) + table.AddRow("SCHEDULE ID", schedule.Id) table.AddSeparator() - table.AddRow("SCHEDULE NAME", utils.PtrString(schedule.Name)) + table.AddRow("SCHEDULE NAME", schedule.Name) table.AddSeparator() - table.AddRow("ENABLED", utils.PtrString(schedule.Enabled)) + table.AddRow("ENABLED", schedule.Enabled) table.AddSeparator() - table.AddRow("RRULE", utils.PtrString(schedule.Rrule)) + table.AddRow("RRULE", schedule.Rrule) table.AddSeparator() if schedule.BackupProperties != nil { - table.AddRow("BACKUP NAME", utils.PtrString(schedule.BackupProperties.Name)) + table.AddRow("BACKUP NAME", schedule.BackupProperties.Name) table.AddSeparator() - table.AddRow("BACKUP RETENTION DAYS", utils.PtrString(schedule.BackupProperties.RetentionPeriod)) + table.AddRow("BACKUP RETENTION DAYS", schedule.BackupProperties.RetentionPeriod) table.AddSeparator() ids := schedule.BackupProperties.VolumeIds - table.AddRow("BACKUP VOLUME IDS", utils.JoinStringPtr(ids, "\n")) + table.AddRow("BACKUP VOLUME IDS", utils.JoinStringPtr(&ids, "\n")) } err := table.Display(p) if err != nil { @@ -152,5 +126,5 @@ func outputResult(p *print.Printer, outputFormat string, schedule serverbackup.B } return nil - } + }) } diff --git a/internal/cmd/server/backup/schedule/describe/describe_test.go b/internal/cmd/server/backup/schedule/describe/describe_test.go index edc952d18..28565edc8 100644 --- a/internal/cmd/server/backup/schedule/describe/describe_test.go +++ b/internal/cmd/server/backup/schedule/describe/describe_test.go @@ -7,16 +7,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupScheduleId = "5" @@ -61,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiGetBackupScheduleRequest)) serverbackup.ApiGetBackupScheduleRequest { - request := testClient.GetBackupSchedule(testCtx, testProjectId, testServerId, testRegion, testBackupScheduleId) + request := testClient.DefaultAPI.GetBackupSchedule(testCtx, testProjectId, testServerId, testRegion, testBackupScheduleId) for _, mod := range mods { mod(&request) } @@ -129,54 +130,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,7 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -257,17 +211,16 @@ func TestOutputResult(t *testing.T) { args: args{ schedule: serverbackup.BackupSchedule{ BackupProperties: &serverbackup.BackupProperties{ - VolumeIds: &[]string{}, + VolumeIds: []string{}, }, }, }, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.schedule); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.schedule); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/schedule/list/list.go b/internal/cmd/server/backup/schedule/list/list.go index edb0c4411..20a7e454e 100644 --- a/internal/cmd/server/backup/schedule/list/list.go +++ b/internal/cmd/server/backup/schedule/list/list.go @@ -2,14 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,7 +21,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) const ( @@ -34,7 +34,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server backup schedules", @@ -48,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all backup schedules for a server with ID "xxx" in JSON format`, "$ stackit server backup schedule list --server-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,7 +64,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -78,17 +78,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server backup schedules: %w", err) } - schedules := *resp.Items - if len(schedules) == 0 { - params.Printer.Info("No backup schedules found for server %s\n", serverLabel) - return nil - } + schedules := resp.GetItems() // Truncate output if model.Limit != nil && len(schedules) > int(*model.Limit) { schedules = schedules[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, schedules) + return outputResult(params.Printer, model.OutputFormat, serverLabel, schedules) }, } configureFlags(cmd) @@ -103,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,42 +119,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiListBackupSchedulesRequest { - req := apiClient.ListBackupSchedules(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.ListBackupSchedules(ctx, model.ProjectId, model.ServerId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, schedules []serverbackup.BackupSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(schedules, "", " ") - if err != nil { - return fmt.Errorf("marshal Server Backup Schedules list: %w", err) +func outputResult(p *print.Printer, outputFormat, serverLabel string, schedules []serverbackup.BackupSchedule) error { + return p.OutputResult(outputFormat, schedules, func() error { + if len(schedules) == 0 { + p.Outputf("No backup schedules found for server %s\n", serverLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(schedules, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Server Backup Schedules list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("SCHEDULE ID", "SCHEDULE NAME", "ENABLED", "RRULE", "BACKUP NAME", "BACKUP RETENTION DAYS", "BACKUP VOLUME IDS") for i := range schedules { @@ -168,16 +143,16 @@ func outputResult(p *print.Printer, outputFormat string, schedules []serverbacku retentionPeriod := "" ids := "" if s.BackupProperties != nil { - backupName = utils.PtrString(s.BackupProperties.Name) - retentionPeriod = utils.PtrString(s.BackupProperties.RetentionPeriod) + backupName = s.BackupProperties.Name + retentionPeriod = string(s.BackupProperties.RetentionPeriod) - ids = utils.JoinStringPtr(s.BackupProperties.VolumeIds, ",") + ids = utils.JoinStringPtr(&s.BackupProperties.VolumeIds, ",") } table.AddRow( - utils.PtrString(s.Id), - utils.PtrString(s.Name), - utils.PtrString(s.Enabled), - utils.PtrString(s.Rrule), + s.Id, + s.Name, + s.Enabled, + s.Rrule, backupName, retentionPeriod, ids, @@ -188,5 +163,5 @@ func outputResult(p *print.Printer, outputFormat string, schedules []serverbacku return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/backup/schedule/list/list_test.go b/internal/cmd/server/backup/schedule/list/list_test.go index c88ec459e..fff446e38 100644 --- a/internal/cmd/server/backup/schedule/list/list_test.go +++ b/internal/cmd/server/backup/schedule/list/list_test.go @@ -4,21 +4,22 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testRegion = "eu01" @@ -53,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiListBackupSchedulesRequest)) serverbackup.ApiListBackupSchedulesRequest { - request := testClient.ListBackupSchedules(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.ListBackupSchedules(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -63,6 +64,7 @@ func fixtureRequest(mods ...func(request *serverbackup.ApiListBackupSchedulesReq func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -117,46 +119,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -180,7 +143,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -192,6 +155,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + serverLabel string schedules []serverbackup.BackupSchedule } tests := []struct { @@ -225,11 +189,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.schedules); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.schedules); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/schedule/schedule.go b/internal/cmd/server/backup/schedule/schedule.go index 1ce797c4f..fd93c4cde 100644 --- a/internal/cmd/server/backup/schedule/schedule.go +++ b/internal/cmd/server/backup/schedule/schedule.go @@ -1,19 +1,19 @@ package schedule import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/schedule/create" del "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/schedule/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/schedule/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/schedule/list" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/schedule/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "schedule", Short: "Provides functionality for Server Backup Schedule", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) diff --git a/internal/cmd/server/backup/schedule/update/update.go b/internal/cmd/server/backup/schedule/update/update.go index bdb3d8040..7df44a83a 100644 --- a/internal/cmd/server/backup/schedule/update/update.go +++ b/internal/cmd/server/backup/schedule/update/update.go @@ -2,11 +2,13 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,10 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverbackup/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) const ( @@ -45,11 +43,11 @@ type inputModel struct { Enabled *bool Rrule *string BackupName *string - BackupRetentionPeriod *int64 + BackupRetentionPeriod *int32 BackupVolumeIds []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", scheduleIdArg), Short: "Updates a Server Backup Schedule", @@ -77,18 +75,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - currentBackupSchedule, err := apiClient.GetBackupScheduleExecute(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupScheduleId) + currentBackupSchedule, err := apiClient.DefaultAPI.GetBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupScheduleId).Execute() if err != nil { params.Printer.Debug(print.ErrorLevel, "get current server backup schedule: %v", err) return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update Server Backup Schedule %q?", model.BackupScheduleId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update Server Backup Schedule %q?", model.BackupScheduleId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -113,7 +109,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(backupScheduleNameFlag, "n", "", "Backup schedule name") cmd.Flags().StringP(backupNameFlag, "b", "", "Backup name") - cmd.Flags().Int64P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") + cmd.Flags().Int32P(backupRetentionPeriodFlag, "d", defaultRetentionPeriod, "Backup retention period (in days)") cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server backup schedule enabled") cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "Backup RRULE (recurrence rule)") cmd.Flags().VarP(flags.UUIDSliceFlag(), backupVolumeIdsFlag, "i", "Backup volume IDs, as comma separated UUID values.") @@ -134,7 +130,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu GlobalFlagModel: globalFlags, BackupScheduleId: scheduleId, ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), - BackupRetentionPeriod: flags.FlagToInt64Pointer(p, cmd, backupRetentionPeriodFlag), + BackupRetentionPeriod: flags.FlagToInt32Pointer(p, cmd, backupRetentionPeriodFlag), BackupScheduleName: flags.FlagToStringPointer(p, cmd, backupScheduleNameFlag), BackupName: flags.FlagToStringPointer(p, cmd, backupNameFlag), Rrule: flags.FlagToStringPointer(p, cmd, rruleFlag), @@ -142,38 +138,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupVolumeIds: flags.FlagToStringSliceValue(p, cmd, backupVolumeIdsFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient, old serverbackup.BackupSchedule) (serverbackup.ApiUpdateBackupScheduleRequest, error) { - req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupScheduleId) + req := apiClient.DefaultAPI.UpdateBackupSchedule(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupScheduleId) if model.BackupName != nil { - old.BackupProperties.Name = model.BackupName + old.BackupProperties.Name = *model.BackupName } if model.BackupRetentionPeriod != nil { - old.BackupProperties.RetentionPeriod = model.BackupRetentionPeriod + old.BackupProperties.RetentionPeriod = *model.BackupRetentionPeriod } if model.BackupVolumeIds != nil { - old.BackupProperties.VolumeIds = &model.BackupVolumeIds + old.BackupProperties.VolumeIds = model.BackupVolumeIds } if model.Enabled != nil { - old.Enabled = model.Enabled + old.Enabled = *model.Enabled } if model.BackupScheduleName != nil { - old.Name = model.BackupScheduleName + old.Name = *model.BackupScheduleName } if model.Rrule != nil { - old.Rrule = model.Rrule + old.Rrule = *model.Rrule } req = req.UpdateBackupSchedulePayload(serverbackup.UpdateBackupSchedulePayload{ @@ -186,25 +174,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbacku } func outputResult(p *print.Printer, outputFormat string, resp serverbackup.BackupSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal update server backup schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal update server backup schedule: %w", err) - } - p.Outputln(string(details)) - + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Updated server backup schedule %d\n", resp.Id) return nil - default: - p.Info("Updated server backup schedule %s\n", utils.PtrString(resp.Id)) - return nil - } + }) } diff --git a/internal/cmd/server/backup/schedule/update/update_test.go b/internal/cmd/server/backup/schedule/update/update_test.go index 263a9188a..95ad5bdbd 100644 --- a/internal/cmd/server/backup/schedule/update/update_test.go +++ b/internal/cmd/server/backup/schedule/update/update_test.go @@ -2,33 +2,31 @@ package update import ( "context" - "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() -var testBackupScheduleId = "5" +var testBackupScheduleId = int32(5) var testRegion = "eu01" func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testBackupScheduleId, + string(testBackupScheduleId), } for _, mod := range mods { mod(argValues) @@ -61,13 +59,13 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - BackupScheduleId: testBackupScheduleId, + BackupScheduleId: string(testBackupScheduleId), ServerId: testServerId, BackupScheduleName: utils.Ptr("example-backup-schedule-name"), Enabled: utils.Ptr(defaultEnabled), Rrule: utils.Ptr(defaultRrule), BackupName: utils.Ptr("example-backup-name"), - BackupRetentionPeriod: utils.Ptr(int64(14)), + BackupRetentionPeriod: utils.Ptr(int32(14)), BackupVolumeIds: []string{testVolumeId}, } for _, mod := range mods { @@ -77,16 +75,15 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureBackupSchedule(mods ...func(schedule *serverbackup.BackupSchedule)) *serverbackup.BackupSchedule { - id, _ := strconv.ParseInt(testBackupScheduleId, 10, 64) schedule := &serverbackup.BackupSchedule{ - Name: utils.Ptr("example-backup-schedule-name"), - Id: utils.Ptr(id), - Enabled: utils.Ptr(defaultEnabled), - Rrule: utils.Ptr(defaultRrule), + Name: "example-backup-schedule-name", + Id: testBackupScheduleId, + Enabled: defaultEnabled, + Rrule: defaultRrule, BackupProperties: &serverbackup.BackupProperties{ - Name: utils.Ptr("example-backup-name"), - RetentionPeriod: utils.Ptr(int64(14)), - VolumeIds: utils.Ptr([]string{testVolumeId}), + Name: "example-backup-name", + RetentionPeriod: int32(14), + VolumeIds: []string{testVolumeId}, }, } for _, mod := range mods { @@ -97,13 +94,13 @@ func fixtureBackupSchedule(mods ...func(schedule *serverbackup.BackupSchedule)) func fixturePayload(mods ...func(payload *serverbackup.UpdateBackupSchedulePayload)) serverbackup.UpdateBackupSchedulePayload { payload := serverbackup.UpdateBackupSchedulePayload{ - Name: utils.Ptr("example-backup-schedule-name"), - Enabled: utils.Ptr(defaultEnabled), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), + Name: "example-backup-schedule-name", + Enabled: defaultEnabled, + Rrule: "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1", BackupProperties: &serverbackup.BackupProperties{ - Name: utils.Ptr("example-backup-name"), - RetentionPeriod: utils.Ptr(int64(14)), - VolumeIds: utils.Ptr([]string{testVolumeId}), + Name: "example-backup-name", + RetentionPeriod: int32(14), + VolumeIds: []string{testVolumeId}, }, } for _, mod := range mods { @@ -113,7 +110,7 @@ func fixturePayload(mods ...func(payload *serverbackup.UpdateBackupSchedulePaylo } func fixtureRequest(mods ...func(request *serverbackup.ApiUpdateBackupScheduleRequest)) serverbackup.ApiUpdateBackupScheduleRequest { - request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testServerId, testRegion, testBackupScheduleId) + request := testClient.DefaultAPI.UpdateBackupSchedule(testCtx, testProjectId, testServerId, testRegion, string(testBackupScheduleId)) request = request.UpdateBackupSchedulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -183,8 +180,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -224,7 +221,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flag groups: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -270,7 +267,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -295,11 +292,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/backup/volume-backup/delete/delete.go b/internal/cmd/server/backup/volume-backup/delete/delete.go index d6e040d56..3fa8aad34 100644 --- a/internal/cmd/server/backup/volume-backup/delete/delete.go +++ b/internal/cmd/server/backup/volume-backup/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", volumeBackupIdArg), Short: "Deletes a Server Volume Backup.", @@ -55,12 +56,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete server volume backup %q? (This cannot be undone)", model.VolumeId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete server volume backup %q? (This cannot be undone)", model.VolumeId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -101,19 +100,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiDeleteVolumeBackupRequest { - req := apiClient.DeleteVolumeBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId, model.VolumeId) + req := apiClient.DefaultAPI.DeleteVolumeBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId, model.VolumeId) return req } diff --git a/internal/cmd/server/backup/volume-backup/delete/delete_test.go b/internal/cmd/server/backup/volume-backup/delete/delete_test.go index 34859934a..6abc17480 100644 --- a/internal/cmd/server/backup/volume-backup/delete/delete_test.go +++ b/internal/cmd/server/backup/volume-backup/delete/delete_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() @@ -65,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiDeleteVolumeBackupRequest)) serverbackup.ApiDeleteVolumeBackupRequest { - request := testClient.DeleteVolumeBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId, testVolumeId) + request := testClient.DefaultAPI.DeleteVolumeBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId, testVolumeId) for _, mod := range mods { mod(&request) } @@ -133,54 +132,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/volume-backup/restore/restore.go b/internal/cmd/server/backup/volume-backup/restore/restore.go index 4d3dd8bc9..e24beee15 100644 --- a/internal/cmd/server/backup/volume-backup/restore/restore.go +++ b/internal/cmd/server/backup/volume-backup/restore/restore.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +16,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) const ( @@ -33,7 +34,7 @@ type inputModel struct { RestoreVolumeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("restore %s", volumeBackupIdArg), Short: "Restore a Server Volume Backup to a volume.", @@ -57,12 +58,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to restore volume backup %q? (This cannot be undone)", model.VolumeBackupId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to restore volume backup %q? (This cannot be undone)", model.VolumeBackupId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -105,22 +104,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu RestoreVolumeId: flags.FlagToStringValue(p, cmd, restoreVolumeIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverbackup.APIClient) serverbackup.ApiRestoreVolumeBackupRequest { - req := apiClient.RestoreVolumeBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId, model.VolumeBackupId) + req := apiClient.DefaultAPI.RestoreVolumeBackup(ctx, model.ProjectId, model.ServerId, model.Region, model.BackupId, model.VolumeBackupId) payload := serverbackup.RestoreVolumeBackupPayload{ - RestoreVolumeId: &model.RestoreVolumeId, + RestoreVolumeId: model.RestoreVolumeId, } req = req.RestoreVolumeBackupPayload(payload) return req diff --git a/internal/cmd/server/backup/volume-backup/restore/restore_test.go b/internal/cmd/server/backup/volume-backup/restore/restore_test.go index 21da9b128..b4beef3e7 100644 --- a/internal/cmd/server/backup/volume-backup/restore/restore_test.go +++ b/internal/cmd/server/backup/volume-backup/restore/restore_test.go @@ -4,20 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverbackup.APIClient{} +var testClient = &serverbackup.APIClient{DefaultAPI: &serverbackup.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testBackupId = uuid.NewString() @@ -68,9 +67,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverbackup.ApiRestoreVolumeBackupRequest)) serverbackup.ApiRestoreVolumeBackupRequest { - request := testClient.RestoreVolumeBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId, testVolumeBackupId) + request := testClient.DefaultAPI.RestoreVolumeBackup(testCtx, testProjectId, testServerId, testRegion, testBackupId, testVolumeBackupId) request = request.RestoreVolumeBackupPayload(serverbackup.RestoreVolumeBackupPayload{ - RestoreVolumeId: &testRestoreVolumeId, + RestoreVolumeId: testRestoreVolumeId, }) for _, mod := range mods { mod(&request) @@ -139,54 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -210,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverbackup.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/backup/volume-backup/volumebackup.go b/internal/cmd/server/backup/volume-backup/volumebackup.go index e0d3b6d62..5bdf0f72d 100644 --- a/internal/cmd/server/backup/volume-backup/volumebackup.go +++ b/internal/cmd/server/backup/volume-backup/volumebackup.go @@ -1,16 +1,16 @@ package volumebackup import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" del "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/volume-backup/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup/volume-backup/restore" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "volume-backup", Short: "Provides functionality for Server Backup Volume Backups", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(del.NewCmd(params)) cmd.AddCommand(restore.NewCmd(params)) } diff --git a/internal/cmd/server/command/command.go b/internal/cmd/server/command/command.go index e6c2d79a8..ccd8978fd 100644 --- a/internal/cmd/server/command/command.go +++ b/internal/cmd/server/command/command.go @@ -1,18 +1,18 @@ package command import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command/create" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command/list" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command/template" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "command", Short: "Provides functionality for Server Command", @@ -24,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) diff --git a/internal/cmd/server/command/create/create.go b/internal/cmd/server/command/create/create.go index 1a766ef46..12757c4ec 100644 --- a/internal/cmd/server/command/create/create.go +++ b/internal/cmd/server/command/create/create.go @@ -2,14 +2,15 @@ package create import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,7 +21,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/client" runcommandUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" ) const ( @@ -37,7 +37,7 @@ type inputModel struct { Params *map[string]string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Server Command", @@ -51,10 +51,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a server command for server with ID "xxx", template name "RunShellScript" and a script provided on the command line`, `$ stackit server command create --server-id xxx --template-name=RunShellScript --params script='echo hello'`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,7 +68,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -76,12 +76,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a Command for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a Command for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -110,7 +108,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -122,56 +120,32 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { CommandTemplateName: flags.FlagToStringValue(p, cmd, commandTemplateNameFlag), Params: flags.FlagToStringToStringPointer(p, cmd, paramsFlag), } - parsedParams, err := runcommandUtils.ParseScriptParams(*model.Params) + + var err error + model.Params, err = runcommandUtils.ParseScriptParams(model.Params) if err != nil { return nil, &cliErr.FlagValidationError{ Flag: paramsFlag, Details: err.Error(), } } - model.Params = &parsedParams - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *runcommand.APIClient) (runcommand.ApiCreateCommandRequest, error) { - req := apiClient.CreateCommand(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.CreateCommand(ctx, model.ProjectId, model.ServerId, model.Region) req = req.CreateCommandPayload(runcommand.CreateCommandPayload{ - CommandTemplateName: &model.CommandTemplateName, + CommandTemplateName: model.CommandTemplateName, Parameters: model.Params, }) return req, nil } func outputResult(p *print.Printer, outputFormat, serverLabel string, resp runcommand.NewCommandResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal server command: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server command: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, resp, func() error { p.Outputf("Created server command for server %s. Command ID: %s\n", serverLabel, utils.PtrString(resp.Id)) return nil - } + }) } diff --git a/internal/cmd/server/command/create/create_test.go b/internal/cmd/server/command/create/create_test.go index eed029584..408dcaeac 100644 --- a/internal/cmd/server/command/create/create_test.go +++ b/internal/cmd/server/command/create/create_test.go @@ -4,21 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &runcommand.APIClient{} +var testClient = &runcommand.APIClient{DefaultAPI: &runcommand.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -59,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *runcommand.ApiCreateCommandRequest)) runcommand.ApiCreateCommandRequest { - request := testClient.CreateCommand(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.CreateCommand(testCtx, testProjectId, testServerId, testRegion) request = request.CreateCommandPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -69,7 +68,7 @@ func fixtureRequest(mods ...func(request *runcommand.ApiCreateCommandRequest)) r func fixturePayload(mods ...func(payload *runcommand.CreateCommandPayload)) runcommand.CreateCommandPayload { payload := runcommand.CreateCommandPayload{ - CommandTemplateName: utils.Ptr("RunShellScript"), + CommandTemplateName: "RunShellScript", Parameters: &map[string]string{"script": "'echo hello'"}, } for _, mod := range mods { @@ -81,6 +80,7 @@ func fixturePayload(mods ...func(payload *runcommand.CreateCommandPayload)) runc func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -102,6 +102,16 @@ func TestParseInput(t *testing.T) { flagValues: map[string]string{}, isValid: false, }, + { + description: "params flag missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, paramsFlag) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Params = nil + }), + }, { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { @@ -127,46 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -198,7 +169,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, runcommand.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -223,11 +194,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/command/describe/describe.go b/internal/cmd/server/command/describe/describe.go index ba09d2575..9b0e25796 100644 --- a/internal/cmd/server/command/describe/describe.go +++ b/internal/cmd/server/command/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" ) const ( @@ -31,7 +31,7 @@ type inputModel struct { CommandId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", commandIdArg), Short: "Shows details of a Server Command", @@ -64,7 +64,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read server command: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -92,42 +92,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CommandId: commandId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *runcommand.APIClient) runcommand.ApiGetCommandRequest { - req := apiClient.GetCommand(ctx, model.ProjectId, model.Region, model.ServerId, model.CommandId) + req := apiClient.DefaultAPI.GetCommand(ctx, model.ProjectId, model.Region, model.ServerId, model.CommandId) return req } -func outputResult(p *print.Printer, outputFormat string, command runcommand.CommandDetails) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(command, "", " ") - if err != nil { - return fmt.Errorf("marshal server command: %w", err) +func outputResult(p *print.Printer, outputFormat string, command *runcommand.CommandDetails) error { + return p.OutputResult(outputFormat, command, func() error { + if command == nil { + return fmt.Errorf("command is nil") } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(command, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server command: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.AddRow("ID", utils.PtrString(command.Id)) table.AddSeparator() @@ -153,5 +132,5 @@ func outputResult(p *print.Printer, outputFormat string, command runcommand.Comm } return nil - } + }) } diff --git a/internal/cmd/server/command/describe/describe_test.go b/internal/cmd/server/command/describe/describe_test.go index 59363f0e7..3440e3efd 100644 --- a/internal/cmd/server/command/describe/describe_test.go +++ b/internal/cmd/server/command/describe/describe_test.go @@ -4,20 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &runcommand.APIClient{} +var testClient = &runcommand.APIClient{DefaultAPI: &runcommand.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -65,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *runcommand.ApiGetCommandRequest)) runcommand.ApiGetCommandRequest { - request := testClient.GetCommand(testCtx, testProjectId, testRegion, testServerId, testCommandId) + request := testClient.DefaultAPI.GetCommand(testCtx, testProjectId, testRegion, testServerId, testCommandId) for _, mod := range mods { mod(&request) } @@ -163,54 +164,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +190,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, runcommand.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -248,7 +202,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string - command runcommand.CommandDetails + command *runcommand.CommandDetails } tests := []struct { name string @@ -256,16 +210,26 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + command: &runcommand.CommandDetails{}, + }, wantErr: false, }, + { + name: "command is nil", + args: args{ + outputFormat: print.PrettyOutputFormat, + command: nil, + }, + wantErr: true, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.command); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.command); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/command/list/list.go b/internal/cmd/server/command/list/list.go index a05d1a034..c50cb0c0b 100644 --- a/internal/cmd/server/command/list/list.go +++ b/internal/cmd/server/command/list/list.go @@ -2,14 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,7 +21,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" ) const ( @@ -34,7 +34,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server commands", @@ -48,9 +48,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all commands for a server with ID "xxx" in JSON format`, "$ stackit server command list --server-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,7 +64,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -78,16 +78,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server commands: %w", err) } - if commands := resp.Items; commands == nil || len(*commands) == 0 { - params.Printer.Info("No commands found for server %s\n", serverLabel) - return nil - } - commands := *resp.Items + + commands := resp.GetItems() + // Truncate output if model.Limit != nil && len(commands) > int(*model.Limit) { commands = commands[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, commands) + return outputResult(params.Printer, model.OutputFormat, serverLabel, commands) }, } configureFlags(cmd) @@ -102,7 +100,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -122,42 +120,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *runcommand.APIClient) runcommand.ApiListCommandsRequest { - req := apiClient.ListCommands(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.ListCommands(ctx, model.ProjectId, model.ServerId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, commands []runcommand.Commands) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(commands, "", " ") - if err != nil { - return fmt.Errorf("marshal server command list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(commands, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server command list: %w", err) +func outputResult(p *print.Printer, outputFormat, serverLabel string, commands []runcommand.Commands) error { + return p.OutputResult(outputFormat, commands, func() error { + if len(commands) == 0 { + p.Outputf("No commands found for server %s\n", serverLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "TEMPLATE NAME", "TEMPLATE TITLE", "STATUS", "STARTED_AT", "FINISHED_AT") for i := range commands { @@ -176,5 +153,5 @@ func outputResult(p *print.Printer, outputFormat string, commands []runcommand.C return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/command/list/list_test.go b/internal/cmd/server/command/list/list_test.go index 7d6ce40fb..c6cafa8f8 100644 --- a/internal/cmd/server/command/list/list_test.go +++ b/internal/cmd/server/command/list/list_test.go @@ -4,21 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &runcommand.APIClient{} +var testClient = &runcommand.APIClient{DefaultAPI: &runcommand.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -56,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *runcommand.ApiListCommandsRequest)) runcommand.ApiListCommandsRequest { - request := testClient.ListCommands(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.ListCommands(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -66,6 +66,7 @@ func fixtureRequest(mods ...func(request *runcommand.ApiListCommandsRequest)) ru func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,46 +121,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -183,7 +145,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, runcommand.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -195,6 +157,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + serverLabel string commands []runcommand.Commands } tests := []struct { @@ -208,11 +171,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.commands); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.commands); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/command/template/describe/describe.go b/internal/cmd/server/command/template/describe/describe.go index 7a83bf609..3c3f6a2b1 100644 --- a/internal/cmd/server/command/template/describe/describe.go +++ b/internal/cmd/server/command/template/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" ) const ( @@ -31,7 +31,7 @@ type inputModel struct { CommandTemplateName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", commandTemplateNameArg), Short: "Shows details of a Server Command Template", @@ -92,42 +92,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu CommandTemplateName: commandTemplateName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *runcommand.APIClient) runcommand.ApiGetCommandTemplateRequest { - req := apiClient.GetCommandTemplate(ctx, model.ProjectId, model.ServerId, model.CommandTemplateName, model.Region) + req := apiClient.DefaultAPI.GetCommandTemplate(ctx, model.ProjectId, model.ServerId, model.CommandTemplateName, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, commandTemplate runcommand.CommandTemplateSchema) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(commandTemplate, "", " ") - if err != nil { - return fmt.Errorf("marshal server command template: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(commandTemplate, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server command template: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, commandTemplate, func() error { table := tables.NewTable() table.AddRow("NAME", utils.PtrString(commandTemplate.Name)) table.AddSeparator() @@ -136,11 +111,11 @@ func outputResult(p *print.Printer, outputFormat string, commandTemplate runcomm table.AddRow("DESCRIPTION", utils.PtrString(commandTemplate.Description)) table.AddSeparator() if commandTemplate.OsType != nil { - table.AddRow("OS TYPE", utils.JoinStringPtr(commandTemplate.OsType, "\n")) + table.AddRow("OS TYPE", utils.JoinStringPtr(&commandTemplate.OsType, "\n")) table.AddSeparator() } - if commandTemplate.ParameterSchema != nil { - table.AddRow("PARAMS", *commandTemplate.ParameterSchema) + if commandTemplate.ParametersSchema != nil { + table.AddRow("PARAMS", *commandTemplate.ParametersSchema) } else { table.AddRow("PARAMS", "") } @@ -151,5 +126,5 @@ func outputResult(p *print.Printer, outputFormat string, commandTemplate runcomm } return nil - } + }) } diff --git a/internal/cmd/server/command/template/describe/describe_test.go b/internal/cmd/server/command/template/describe/describe_test.go index 9fa7ba208..bd4cacb53 100644 --- a/internal/cmd/server/command/template/describe/describe_test.go +++ b/internal/cmd/server/command/template/describe/describe_test.go @@ -4,20 +4,20 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &runcommand.APIClient{} +var testClient = &runcommand.APIClient{DefaultAPI: &runcommand.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -65,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *runcommand.ApiGetCommandTemplateRequest)) runcommand.ApiGetCommandTemplateRequest { - request := testClient.GetCommandTemplate(testCtx, testProjectId, testServerId, testCommandTemplateName, testRegion) + request := testClient.DefaultAPI.GetCommandTemplate(testCtx, testProjectId, testServerId, testCommandTemplateName, testRegion) for _, mod := range mods { mod(&request) } @@ -163,54 +163,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -236,7 +189,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, runcommand.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -261,11 +214,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.commandTemplate); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.commandTemplate); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/command/template/list/list.go b/internal/cmd/server/command/template/list/list.go index a085c3210..6258a0608 100644 --- a/internal/cmd/server/command/template/list/list.go +++ b/internal/cmd/server/command/template/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/runcommand/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" ) const ( @@ -29,7 +29,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server command templates", @@ -43,9 +43,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all commands templates in JSON format`, "$ stackit server command template list --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -62,11 +62,8 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server command templates: %w", err) } - if templates := resp.Items; templates == nil || len(*templates) == 0 { - params.Printer.Info("No commands templates found\n") - return nil - } - templates := *resp.Items + + templates := resp.GetItems() // Truncate output if model.Limit != nil && len(templates) > int(*model.Limit) { @@ -83,7 +80,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -102,50 +99,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, _ *inputModel, apiClient *runcommand.APIClient) runcommand.ApiListCommandTemplatesRequest { - req := apiClient.ListCommandTemplates(ctx) + req := apiClient.DefaultAPI.ListCommandTemplates(ctx) return req } func outputResult(p *print.Printer, outputFormat string, templates []runcommand.CommandTemplate) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(templates, "", " ") - if err != nil { - return fmt.Errorf("marshal server command template list: %w", err) + return p.OutputResult(outputFormat, templates, func() error { + if len(templates) == 0 { + p.Outputf("No commands templates found\n") + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(templates, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server command template list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("NAME", "OS TYPE", "TITLE") - for i := range templates { - s := templates[i] - + for _, s := range templates { var osType string - if s.OsType != nil && len(*s.OsType) > 0 { - osType = utils.JoinStringPtr(s.OsType, ",") + if len(s.OsType) > 0 { + osType = utils.JoinStringPtr(&s.OsType, ",") } table.AddRow( @@ -159,5 +133,5 @@ func outputResult(p *print.Printer, outputFormat string, templates []runcommand. return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/command/template/list/list_test.go b/internal/cmd/server/command/template/list/list_test.go index 0ff491e5d..895bb5f23 100644 --- a/internal/cmd/server/command/template/list/list_test.go +++ b/internal/cmd/server/command/template/list/list_test.go @@ -4,21 +4,21 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &runcommand.APIClient{} +var testClient = &runcommand.APIClient{DefaultAPI: &runcommand.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { @@ -47,7 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *runcommand.ApiListCommandTemplatesRequest)) runcommand.ApiListCommandTemplatesRequest { - request := testClient.ListCommandTemplates(testCtx) + request := testClient.DefaultAPI.ListCommandTemplates(testCtx) for _, mod := range mods { mod(&request) } @@ -57,6 +57,7 @@ func fixtureRequest(mods ...func(request *runcommand.ApiListCommandTemplatesRequ func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -111,46 +112,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -174,7 +136,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, runcommand.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -207,11 +169,10 @@ func TestOutputResult(t *testing.T) { }, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.templates); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.templates); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/command/template/template.go b/internal/cmd/server/command/template/template.go index 6b41f434b..5607fa5c4 100644 --- a/internal/cmd/server/command/template/template.go +++ b/internal/cmd/server/command/template/template.go @@ -1,16 +1,16 @@ package template import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command/template/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command/template/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "template", Short: "Provides functionality for Server Command Template", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) } diff --git a/internal/cmd/server/console/console.go b/internal/cmd/server/console/console.go index 1462a97ac..3fb53c919 100644 --- a/internal/cmd/server/console/console.go +++ b/internal/cmd/server/console/console.go @@ -2,12 +2,13 @@ package console import ( "context" - "encoding/json" "fmt" "net/url" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("console %s", serverIdArg), Short: "Gets a URL for server remote console", @@ -59,7 +59,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -74,7 +74,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("server console: %w", err) } - return outputResult(params.Printer, model.OutputFormat, serverLabel, *resp) + return outputResult(params.Printer, model.OutputFormat, serverLabel, resp) }, } return cmd @@ -93,41 +93,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetServerConsoleRequest { - return apiClient.GetServerConsole(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.GetServerConsole(ctx, model.ProjectId, model.Region, model.ServerId) } -func outputResult(p *print.Printer, outputFormat, serverLabel string, serverUrl iaas.ServerConsoleUrl) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(serverUrl, "", " ") - if err != nil { - return fmt.Errorf("marshal url: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(serverUrl, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal url: %w", err) - } - p.Outputln(string(details)) - - return nil - default: +func outputResult(p *print.Printer, outputFormat, serverLabel string, serverUrl *iaas.ServerConsoleUrl) error { + return p.OutputResult(outputFormat, serverUrl, func() error { if _, ok := serverUrl.GetUrlOk(); !ok { return fmt.Errorf("server url is nil") } @@ -140,5 +115,5 @@ func outputResult(p *print.Printer, outputFormat, serverLabel string, serverUrl p.Outputf("Remote console URL %q for server %q\n", unescapedURL, serverLabel) return nil - } + }) } diff --git a/internal/cmd/server/console/console_test.go b/internal/cmd/server/console/console_test.go index 9b6c0413c..3a49d0002 100644 --- a/internal/cmd/server/console/console_test.go +++ b/internal/cmd/server/console/console_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -36,7 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -49,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, } @@ -59,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetServerConsoleRequest)) iaas.ApiGetServerConsoleRequest { - request := testClient.GetServerConsole(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.GetServerConsole(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -97,7 +100,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -105,7 +108,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -129,54 +132,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -212,7 +168,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat, serverLabel string - serverUrl iaas.ServerConsoleUrl + serverUrl *iaas.ServerConsoleUrl } tests := []struct { name string @@ -227,18 +183,17 @@ func TestOutputResult(t *testing.T) { { name: "set server url", args: args{ - serverUrl: iaas.ServerConsoleUrl{ - Url: utils.Ptr(""), + serverUrl: &iaas.ServerConsoleUrl{ + Url: "", }, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.serverUrl); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.serverUrl); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/create/create.go b/internal/cmd/server/create/create.go index 882c971cb..fdd623727 100644 --- a/internal/cmd/server/create/create.go +++ b/internal/cmd/server/create/create.go @@ -2,11 +2,14 @@ package create import ( "context" - "encoding/json" + "encoding/base64" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +20,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -44,29 +45,37 @@ const ( volumesFlag = "volumes" ) +var agentProvisioningPolicyFlag = flags.StringEnumFlag( + "agent-provisioning-policy", + []string{"ALWAYS", "NEVER", "INHERIT"}, + "Whether to provision an agent on server creation,", + flags.StringEnumDefaultValue("INHERIT"), +) + type inputModel struct { *globalflags.GlobalFlagModel - Name *string - MachineType *string + Name string + MachineType string AffinityGroup *string AvailabilityZone *string - BootVolumeSourceId *string - BootVolumeSourceType *string + AgentProvisioningPolicy *string + BootVolumeSourceId string + BootVolumeSourceType string BootVolumeSize *int64 BootVolumePerformanceClass *string BootVolumeDeleteOnTermination *bool ImageId *string KeypairName *string - Labels *map[string]string + Labels map[string]any NetworkId *string - NetworkInterfaceIds *[]string - SecurityGroups *[]string - ServiceAccountMails *[]string + NetworkInterfaceIds []string + SecurityGroups []string + ServiceAccountMails []string UserData *string - Volumes *[]string + Volumes []string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a server", @@ -74,45 +83,37 @@ func NewCmd(params *params.CmdParams) *cobra.Command { Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Create a server from an image with id xxx`, - `$ stackit server create --machine-type t1.1 --name server1 --image-id xxx`, - ), - examples.NewExample( - `Create a server with labels from an image with id xxx`, - `$ stackit server create --machine-type t1.1 --name server1 --image-id xxx --labels key=value,foo=bar`, - ), - examples.NewExample( - `Create a server with a boot volume`, - `$ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64`, + `Create a server with a boot volume with source type image`, + `$ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64`, ), examples.NewExample( `Create a server with a boot volume from an existing volume`, - `$ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type volume`, + `$ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type volume`, ), examples.NewExample( `Create a server with a keypair`, - `$ stackit server create --machine-type t1.1 --name server1 --image-id xxx --keypair-name example`, - ), - examples.NewExample( - `Create a server with a network`, - `$ stackit server create --machine-type t1.1 --name server1 --image-id xxx --network-id yyy`, + `$ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --keypair-name example`, ), examples.NewExample( `Create a server with a network interface`, - `$ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --network-interface-ids yyy`, + `$ stackit server create --machine-type g2i.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --network-interface-ids yyy`, ), examples.NewExample( `Create a server with an attached volume`, - `$ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --volumes yyy`, + `$ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --volumes zzz`, ), examples.NewExample( `Create a server with user data (cloud-init)`, - `$ stackit server create --machine-type t1.1 --name server1 --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --user-data @path/to/file.yaml")`, + `$ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --user-data @path/to/file.yaml`, + ), + examples.NewExample( + `Create a server with provisioned agent`, + `$ stackit server create --machine-type g2i.1 --name server1 --network-id yyy --boot-volume-source-id xxx --boot-volume-source-type image --boot-volume-size 64 --agent-provisioning-policy ALWAYS`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -129,12 +130,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a server for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a server for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -147,16 +146,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating server") - _, err = wait.CreateServerWaitHandler(ctx, apiClient, model.ProjectId, serverId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating server", func() error { + _, err = wait.CreateServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, serverId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server creation: %w", err) } - s.Stop() } - return outputResult(params.Printer, model.OutputFormat, projectLabel, resp) + return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) }, } configureFlags(cmd) @@ -164,8 +163,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { + agentProvisioningPolicyFlag.Register(cmd.Flags()) cmd.Flags().StringP(nameFlag, "n", "", "Server name") - cmd.Flags().String(machineTypeFlag, "", "Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html") + cmd.Flags().String(machineTypeFlag, "", "Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/") cmd.Flags().String(affinityGroupFlag, "", "The affinity group the server is assigned to") cmd.Flags().String(availabilityZoneFlag, "", "The availability zone of the server") cmd.Flags().String(bootVolumeSourceIdFlag, "", "ID of the source object of boot volume. It can be either an image or volume ID") @@ -187,10 +187,16 @@ func configureFlags(cmd *cobra.Command) { cmd.MarkFlagsMutuallyExclusive(imageIdFlag, bootVolumeSourceIdFlag) cmd.MarkFlagsMutuallyExclusive(imageIdFlag, bootVolumeSourceTypeFlag) cmd.MarkFlagsMutuallyExclusive(networkIdFlag, networkInterfaceIdsFlag) + cmd.MarkFlagsOneRequired(networkIdFlag, networkInterfaceIdsFlag) + + // hide image-id flag from help command to prevent users from using it alone which leads to errors, because it works only with small images + // instead of using image-id alone, boot-volume flags can be used with type image + cobra.CheckErr(cmd.Flags().MarkHidden(imageIdFlag)) + cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -247,44 +253,37 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - Name: flags.FlagToStringPointer(p, cmd, nameFlag), - MachineType: flags.FlagToStringPointer(p, cmd, machineTypeFlag), + Name: flags.FlagToStringValue(p, cmd, nameFlag), + MachineType: flags.FlagToStringValue(p, cmd, machineTypeFlag), AffinityGroup: flags.FlagToStringPointer(p, cmd, affinityGroupFlag), AvailabilityZone: flags.FlagToStringPointer(p, cmd, availabilityZoneFlag), - BootVolumeSourceId: flags.FlagToStringPointer(p, cmd, bootVolumeSourceIdFlag), - BootVolumeSourceType: flags.FlagToStringPointer(p, cmd, bootVolumeSourceTypeFlag), + AgentProvisioningPolicy: agentProvisioningPolicyFlag.Ptr(), + BootVolumeSourceId: flags.FlagToStringValue(p, cmd, bootVolumeSourceIdFlag), + BootVolumeSourceType: flags.FlagToStringValue(p, cmd, bootVolumeSourceTypeFlag), BootVolumeSize: flags.FlagToInt64Pointer(p, cmd, bootVolumeSizeFlag), BootVolumePerformanceClass: flags.FlagToStringPointer(p, cmd, bootVolumePerformanceClassFlag), BootVolumeDeleteOnTermination: flags.FlagToBoolPointer(p, cmd, bootVolumeDeleteOnTerminationFlag), ImageId: flags.FlagToStringPointer(p, cmd, imageIdFlag), KeypairName: flags.FlagToStringPointer(p, cmd, keypairNameFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag), - NetworkInterfaceIds: flags.FlagToStringSlicePointer(p, cmd, networkInterfaceIdsFlag), - SecurityGroups: flags.FlagToStringSlicePointer(p, cmd, securityGroupsFlag), - ServiceAccountMails: flags.FlagToStringSlicePointer(p, cmd, serviceAccountEmailsFlag), + NetworkInterfaceIds: flags.FlagToStringSliceValue(p, cmd, networkInterfaceIdsFlag), + SecurityGroups: flags.FlagToStringSliceValue(p, cmd, securityGroupsFlag), + ServiceAccountMails: flags.FlagToStringSliceValue(p, cmd, serviceAccountEmailsFlag), UserData: flags.FlagToStringPointer(p, cmd, userDataFlag), - Volumes: flags.FlagToStringSlicePointer(p, cmd, volumesFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Volumes: flags.FlagToStringSliceValue(p, cmd, volumesFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateServerRequest { - req := apiClient.CreateServer(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreateServer(ctx, model.ProjectId, model.Region) - var userData *[]byte + var userData *string if model.UserData != nil { - userData = utils.Ptr([]byte(*model.UserData)) + userData = utils.Ptr(base64.StdEncoding.EncodeToString([]byte(*model.UserData))) } payload := iaas.CreateServerPayload{ @@ -299,11 +298,20 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli ServiceAccountMails: model.ServiceAccountMails, UserData: userData, Volumes: model.Volumes, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, + } + + if model.AgentProvisioningPolicy != nil { + switch *model.AgentProvisioningPolicy { + case "ALWAYS": + payload.Agent = &iaas.ServerAgent{Provisioned: utils.Ptr(true)} + case "NEVER": + payload.Agent = &iaas.ServerAgent{Provisioned: utils.Ptr(false)} + } } - if model.BootVolumePerformanceClass != nil || model.BootVolumeSize != nil || model.BootVolumeDeleteOnTermination != nil || model.BootVolumeSourceId != nil || model.BootVolumeSourceType != nil { - payload.BootVolume = &iaas.CreateServerPayloadBootVolume{ + if model.BootVolumePerformanceClass != nil || model.BootVolumeSize != nil || model.BootVolumeDeleteOnTermination != nil || (model.BootVolumeSourceId != "" && model.BootVolumeSourceType != "") { + payload.BootVolume = &iaas.BootVolume{ PerformanceClass: model.BootVolumePerformanceClass, Size: model.BootVolumeSize, DeleteOnTermination: model.BootVolumeDeleteOnTermination, @@ -315,7 +323,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } if model.NetworkInterfaceIds != nil || model.NetworkId != nil { - payload.Networking = &iaas.CreateServerPayloadNetworking{} + payload.Networking = iaas.CreateServerPayloadAllOfNetworking{} if model.NetworkInterfaceIds != nil { payload.Networking.CreateServerNetworkingWithNics = &iaas.CreateServerNetworkingWithNics{ @@ -332,29 +340,16 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req.CreateServerPayload(payload) } -func outputResult(p *print.Printer, outputFormat, projectLabel string, server *iaas.Server) error { +func outputResult(p *print.Printer, outputFormat string, async bool, projectLabel string, server *iaas.Server) error { if server == nil { return fmt.Errorf("server response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(server, "", " ") - if err != nil { - return fmt.Errorf("marshal server: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(server, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server: %w", err) + return p.OutputResult(outputFormat, server, func() error { + operationState := "Created" + if async { + operationState = "Triggered creation of" } - p.Outputln(string(details)) - + p.Outputf("%s server for project %q.\nServer ID: %s\n", operationState, projectLabel, utils.PtrString(server.Id)) return nil - default: - p.Outputf("Created server for project %q.\nServer ID: %s\n", projectLabel, utils.PtrString(server.Id)) - return nil - } + }) } diff --git a/internal/cmd/server/create/create_test.go b/internal/cmd/server/create/create_test.go index 1548691d4..a79f69b03 100644 --- a/internal/cmd/server/create/create_test.go +++ b/internal/cmd/server/create/create_test.go @@ -4,23 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testSourceId = uuid.NewString() @@ -30,24 +32,26 @@ var testVolumeId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - availabilityZoneFlag: "eu01-1", - nameFlag: "test-server-name", - machineTypeFlag: "t1.1", - affinityGroupFlag: "test-affinity-group", - labelFlag: "key=value", - bootVolumePerformanceClassFlag: "test-perf-class", - bootVolumeSizeFlag: "5", - bootVolumeSourceIdFlag: testSourceId, - bootVolumeSourceTypeFlag: "test-source-type", - bootVolumeDeleteOnTerminationFlag: "false", - imageIdFlag: testImageId, - keypairNameFlag: "test-keypair-name", - networkIdFlag: testNetworkId, - securityGroupsFlag: "test-security-groups", - serviceAccountEmailsFlag: "test-service-account", - userDataFlag: "test-user-data", - volumesFlag: testVolumeId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + agentProvisioningPolicyFlag.Name(): "INHERIT", + availabilityZoneFlag: "eu01-1", + nameFlag: "test-server-name", + machineTypeFlag: "t1.1", + affinityGroupFlag: "test-affinity-group", + labelFlag: "key=value", + bootVolumePerformanceClassFlag: "test-perf-class", + bootVolumeSizeFlag: "5", + bootVolumeSourceIdFlag: testSourceId, + bootVolumeSourceTypeFlag: "test-source-type", + bootVolumeDeleteOnTerminationFlag: "false", + keypairNameFlag: "test-keypair-name", + networkIdFlag: testNetworkId, + securityGroupsFlag: "test-security-groups", + serviceAccountEmailsFlag: "test-service-account", + userDataFlag: "test-user-data", + volumesFlag: testVolumeId, } for _, mod := range mods { mod(flagValues) @@ -59,27 +63,28 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, + AgentProvisioningPolicy: utils.Ptr("INHERIT"), AvailabilityZone: utils.Ptr("eu01-1"), - Name: utils.Ptr("test-server-name"), - MachineType: utils.Ptr("t1.1"), + Name: "test-server-name", + MachineType: "t1.1", AffinityGroup: utils.Ptr("test-affinity-group"), BootVolumePerformanceClass: utils.Ptr("test-perf-class"), BootVolumeSize: utils.Ptr(int64(5)), - BootVolumeSourceId: utils.Ptr(testSourceId), - BootVolumeSourceType: utils.Ptr("test-source-type"), + BootVolumeSourceId: testSourceId, + BootVolumeSourceType: "test-source-type", BootVolumeDeleteOnTermination: utils.Ptr(false), - ImageId: utils.Ptr(testImageId), KeypairName: utils.Ptr("test-keypair-name"), NetworkId: utils.Ptr(testNetworkId), - SecurityGroups: utils.Ptr([]string{"test-security-groups"}), - ServiceAccountMails: utils.Ptr([]string{"test-service-account"}), + SecurityGroups: []string{"test-security-groups"}, + ServiceAccountMails: []string{"test-service-account"}, UserData: utils.Ptr("test-user-data"), - Volumes: utils.Ptr([]string{testVolumeId}), - Labels: utils.Ptr(map[string]string{ + Volumes: []string{testVolumeId}, + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -88,7 +93,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateServerRequest)) iaas.ApiCreateServerRequest { - request := testClient.CreateServer(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateServer(testCtx, testProjectId, testRegion) request = request.CreateServerPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -97,10 +102,10 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateServerRequest)) iaas.Api } func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateServerRequest)) iaas.ApiCreateServerRequest { - request := testClient.CreateServer(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateServer(testCtx, testProjectId, testRegion) request = request.CreateServerPayload(iaas.CreateServerPayload{ - MachineType: utils.Ptr("t1.1"), - Name: utils.Ptr("test-server-name"), + MachineType: "t1.1", + Name: "test-server-name", }) for _, mod := range mods { mod(&request) @@ -110,29 +115,28 @@ func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateServerRequest)) func fixturePayload(mods ...func(payload *iaas.CreateServerPayload)) iaas.CreateServerPayload { payload := iaas.CreateServerPayload{ - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), - MachineType: utils.Ptr("t1.1"), - Name: utils.Ptr("test-server-name"), + }, + MachineType: "t1.1", + Name: "test-server-name", AvailabilityZone: utils.Ptr("eu01-1"), AffinityGroup: utils.Ptr("test-affinity-group"), - ImageId: utils.Ptr(testImageId), KeypairName: utils.Ptr("test-keypair-name"), - SecurityGroups: utils.Ptr([]string{"test-security-groups"}), - ServiceAccountMails: utils.Ptr([]string{"test-service-account"}), - UserData: utils.Ptr([]byte("test-user-data")), - Volumes: utils.Ptr([]string{testVolumeId}), - BootVolume: &iaas.CreateServerPayloadBootVolume{ + SecurityGroups: []string{"test-security-groups"}, + ServiceAccountMails: []string{"test-service-account"}, + UserData: utils.Ptr("dGVzdC11c2VyLWRhdGE="), + Volumes: []string{testVolumeId}, + BootVolume: &iaas.BootVolume{ PerformanceClass: utils.Ptr("test-perf-class"), Size: utils.Ptr(int64(5)), DeleteOnTermination: utils.Ptr(false), Source: &iaas.BootVolumeSource{ - Id: utils.Ptr(testSourceId), - Type: utils.Ptr("test-source-type"), + Id: testSourceId, + Type: "test-source-type", }, }, - Networking: &iaas.CreateServerPayloadNetworking{ + Networking: iaas.CreateServerPayloadAllOfNetworking{ CreateServerNetworking: &iaas.CreateServerNetworking{ NetworkId: utils.Ptr(testNetworkId), }, @@ -147,6 +151,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateServerPayload)) iaas.Create func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -161,6 +166,7 @@ func TestParseInput(t *testing.T) { description: "required only", flagValues: fixtureFlagValues(func(flagValues map[string]string) { delete(flagValues, affinityGroupFlag) + delete(flagValues, agentProvisioningPolicyFlag.Name()) delete(flagValues, availabilityZoneFlag) delete(flagValues, labelFlag) delete(flagValues, bootVolumeSourceIdFlag) @@ -169,30 +175,31 @@ func TestParseInput(t *testing.T) { delete(flagValues, bootVolumePerformanceClassFlag) delete(flagValues, bootVolumeDeleteOnTerminationFlag) delete(flagValues, keypairNameFlag) - delete(flagValues, networkIdFlag) delete(flagValues, networkInterfaceIdsFlag) delete(flagValues, securityGroupsFlag) delete(flagValues, serviceAccountEmailsFlag) delete(flagValues, userDataFlag) delete(flagValues, volumesFlag) + flagValues[imageIdFlag] = testImageId }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { model.AffinityGroup = nil + model.AgentProvisioningPolicy = utils.Ptr("INHERIT") model.AvailabilityZone = nil model.Labels = nil - model.BootVolumeSourceId = nil - model.BootVolumeSourceType = nil + model.BootVolumeSourceId = "" + model.BootVolumeSourceType = "" model.BootVolumeSize = nil model.BootVolumePerformanceClass = nil model.BootVolumeDeleteOnTermination = nil model.KeypairName = nil - model.NetworkId = nil model.NetworkInterfaceIds = nil model.SecurityGroups = nil model.ServiceAccountMails = nil model.UserData = nil model.Volumes = nil + model.ImageId = utils.Ptr(testImageId) }), }, { @@ -217,21 +224,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -245,8 +252,8 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { model.NetworkId = utils.Ptr(testNetworkId) - model.Name = utils.Ptr("test-server-name") - model.MachineType = utils.Ptr("t1.1") + model.Name = "test-server-name" + model.MachineType = "t1.1" }), }, { @@ -257,8 +264,8 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.BootVolumeSourceId = utils.Ptr(testImageId) - model.BootVolumeSourceType = utils.Ptr("image") + model.BootVolumeSourceId = testImageId + model.BootVolumeSourceType = "image" }), }, { @@ -292,12 +299,14 @@ func TestParseInput(t *testing.T) { delete(flagValues, bootVolumeSourceIdFlag) delete(flagValues, bootVolumeSourceTypeFlag) delete(flagValues, bootVolumeSizeFlag) + flagValues[imageIdFlag] = testImageId }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.BootVolumeSourceId = nil - model.BootVolumeSourceType = nil + model.BootVolumeSourceId = "" + model.BootVolumeSourceType = "" model.BootVolumeSize = nil + model.ImageId = utils.Ptr(testImageId) }), }, { @@ -322,50 +331,31 @@ func TestParseInput(t *testing.T) { model.ImageId = nil }), }, + { + description: "valid with agent-provisioned flag missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, agentProvisioningPolicyFlag.Name()) + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AgentProvisioningPolicy = utils.Ptr("INHERIT") + }), + }, + { + description: "agent-provisioned flag properly handled", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[agentProvisioningPolicyFlag.Name()] = "ALWAYS" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AgentProvisioningPolicy = utils.Ptr("ALWAYS") + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -386,13 +376,38 @@ func TestBuildRequest(t *testing.T) { model: &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - MachineType: utils.Ptr("t1.1"), - Name: utils.Ptr("test-server-name"), + MachineType: "t1.1", + Name: "test-server-name", }, expectedRequest: fixtureRequiredRequest(), }, + { + description: "with provisioned agent", + model: fixtureInputModel(func(model *inputModel) { + model.AgentProvisioningPolicy = utils.Ptr("ALWAYS") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateServerRequest) { + payload := fixturePayload() + payload.Agent = &iaas.ServerAgent{ + Provisioned: utils.Ptr(true), + } + *request = request.CreateServerPayload(payload) + }), + }, + { + description: "with user data", + model: fixtureInputModel(func(model *inputModel) { + model.UserData = utils.Ptr("cloud-init data") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiCreateServerRequest) { + payload := fixturePayload() + payload.UserData = utils.Ptr("Y2xvdWQtaW5pdCBkYXRh") + *request = request.CreateServerPayload(payload) + }), + }, } for _, tt := range tests { @@ -401,7 +416,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -413,6 +428,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + async bool projectLabel string server *iaas.Server } @@ -434,11 +450,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.server); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.server); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/deallocate/deallocate.go b/internal/cmd/server/deallocate/deallocate.go index b614ba471..d6f710e29 100644 --- a/internal/cmd/server/deallocate/deallocate.go +++ b/internal/cmd/server/deallocate/deallocate.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("deallocate %s", serverIdArg), Short: "Deallocates an existing server", @@ -54,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -62,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to deallocate server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to deallocate server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -79,13 +79,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deallocating server") - _, err = wait.DeallocateServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deallocating server", func() error { + _, err = wait.DeallocateServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server deallocating: %w", err) } - s.Stop() } operationState := "Deallocated" @@ -113,18 +113,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeallocateServerRequest { - return apiClient.DeallocateServer(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.DeallocateServer(ctx, model.ProjectId, model.Region, model.ServerId) } diff --git a/internal/cmd/server/deallocate/deallocate_test.go b/internal/cmd/server/deallocate/deallocate_test.go index 379fc3a58..5e882a1b7 100644 --- a/internal/cmd/server/deallocate/deallocate_test.go +++ b/internal/cmd/server/deallocate/deallocate_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +49,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, + Region: testRegion, ProjectId: testProjectId, }, ServerId: testServerId, @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeallocateServerRequest)) iaas.ApiDeallocateServerRequest { - request := testClient.DeallocateServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.DeallocateServer(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -96,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -104,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -128,54 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/delete/delete.go b/internal/cmd/server/delete/delete.go index c5aae8216..d09e219aa 100644 --- a/internal/cmd/server/delete/delete.go +++ b/internal/cmd/server/delete/delete.go @@ -4,7 +4,13 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,10 +20,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" - - "github.com/spf13/cobra" ) const ( @@ -29,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", serverIdArg), Short: "Deletes a server", @@ -57,7 +59,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -65,12 +67,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -82,13 +82,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting server") - _, err = wait.DeleteServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting server", func() error { + _, err = wait.DeleteServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -115,18 +115,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteServerRequest { - return apiClient.DeleteServer(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.DeleteServer(ctx, model.ProjectId, model.Region, model.ServerId) } diff --git a/internal/cmd/server/delete/delete_test.go b/internal/cmd/server/delete/delete_test.go index 1c04e26cb..e7a01ccaf 100644 --- a/internal/cmd/server/delete/delete_test.go +++ b/internal/cmd/server/delete/delete_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testServerId = uuid.NewString() var testProjectId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteServerRequest)) iaas.ApiDeleteServerRequest { - request := testClient.DeleteServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.DeleteServer(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -102,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/describe/describe.go b/internal/cmd/server/describe/describe.go index 85444e6ec..0bfe115fb 100644 --- a/internal/cmd/server/describe/describe.go +++ b/internal/cmd/server/describe/describe.go @@ -6,7 +6,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,7 @@ import ( "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -30,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", serverIdArg), Short: "Shows details of a server", @@ -85,20 +86,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetServerRequest { - req := apiClient.GetServer(ctx, model.ProjectId, model.ServerId) + req := apiClient.DefaultAPI.GetServer(ctx, model.ProjectId, model.Region, model.ServerId) req = req.Details(true) return req @@ -133,7 +126,7 @@ func outputResult(p *print.Printer, outputFormat string, server *iaas.Server) er table.AddRow("ID", utils.PtrString(server.Id)) table.AddSeparator() - table.AddRow("NAME", utils.PtrString(server.Name)) + table.AddRow("NAME", server.Name) table.AddSeparator() table.AddRow("STATE", utils.PtrString(server.Status)) table.AddSeparator() @@ -163,44 +156,47 @@ func outputResult(p *print.Printer, outputFormat string, server *iaas.Server) er table.AddSeparator() } - if server.MachineType != nil { - table.AddRow("MACHINE TYPE", *server.MachineType) + if server.Agent != nil && server.Agent.Provisioned != nil { + table.AddRow("AGENT", *server.Agent.Provisioned) table.AddSeparator() } - if server.Labels != nil && len(*server.Labels) > 0 { + table.AddRow("MACHINE TYPE", server.MachineType) + table.AddSeparator() + + if len(server.Labels) > 0 { labels := []string{} - for key, value := range *server.Labels { + for key, value := range server.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) table.AddSeparator() } - if server.ServiceAccountMails != nil && len(*server.ServiceAccountMails) > 0 { - table.AddRow("SERVICE ACCOUNTS", strings.Join(*server.ServiceAccountMails, "\n")) + if len(server.ServiceAccountMails) > 0 { + table.AddRow("SERVICE ACCOUNTS", strings.Join(server.ServiceAccountMails, "\n")) table.AddSeparator() } - if server.Volumes != nil && len(*server.Volumes) > 0 { + if len(server.Volumes) > 0 { volumes := []string{} - volumes = append(volumes, *server.Volumes...) + volumes = append(volumes, server.Volumes...) table.AddRow("VOLUMES", strings.Join(volumes, "\n")) table.AddSeparator() } content = append(content, table) - if server.Nics != nil && len(*server.Nics) > 0 { + if len(server.Nics) > 0 { nicsTable := tables.NewTable() nicsTable.SetTitle("Attached Network Interfaces") nicsTable.SetHeader("ID", "NETWORK ID", "NETWORK NAME", "IPv4", "PUBLIC IP") - for _, nic := range *server.Nics { + for _, nic := range server.Nics { nicsTable.AddRow( - utils.PtrString(nic.NicId), - utils.PtrString(nic.NetworkId), - utils.PtrString(nic.NetworkName), + nic.NicId, + nic.NetworkId, + nic.NetworkName, utils.PtrString(nic.Ipv4), utils.PtrString(nic.PublicIp), ) @@ -214,28 +210,23 @@ func outputResult(p *print.Printer, outputFormat string, server *iaas.Server) er maintenanceWindow := tables.NewTable() maintenanceWindow.SetTitle("Maintenance Window") - if server.MaintenanceWindow.Status != nil { - maintenanceWindow.AddRow("STATUS", *server.MaintenanceWindow.Status) - maintenanceWindow.AddSeparator() - } + maintenanceWindow.AddRow("STATUS", server.MaintenanceWindow.Status) + maintenanceWindow.AddSeparator() + if server.MaintenanceWindow.Details != nil { maintenanceWindow.AddRow("DETAILS", *server.MaintenanceWindow.Details) maintenanceWindow.AddSeparator() } - if server.MaintenanceWindow.StartsAt != nil { - maintenanceWindow.AddRow( - "STARTS AT", - utils.ConvertTimePToDateTimeString(server.MaintenanceWindow.StartsAt), - ) - maintenanceWindow.AddSeparator() - } - if server.MaintenanceWindow.EndsAt != nil { - maintenanceWindow.AddRow( - "ENDS AT", - utils.ConvertTimePToDateTimeString(server.MaintenanceWindow.EndsAt), - ) - maintenanceWindow.AddSeparator() - } + maintenanceWindow.AddRow( + "STARTS AT", + utils.ConvertTimePToDateTimeString(&server.MaintenanceWindow.StartsAt), + ) + maintenanceWindow.AddSeparator() + maintenanceWindow.AddRow( + "ENDS AT", + utils.ConvertTimePToDateTimeString(&server.MaintenanceWindow.EndsAt), + ) + maintenanceWindow.AddSeparator() content = append(content, maintenanceWindow) } diff --git a/internal/cmd/server/describe/describe_test.go b/internal/cmd/server/describe/describe_test.go index 86052e8b7..80f4be4c9 100644 --- a/internal/cmd/server/describe/describe_test.go +++ b/internal/cmd/server/describe/describe_test.go @@ -4,22 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -35,7 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, ServerId: testServerId, @@ -58,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetServerRequest)) iaas.ApiGetServerRequest { - request := testClient.GetServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.GetServer(testCtx, testProjectId, testRegion, testServerId) request = request.Details(true) for _, mod := range mods { mod(&request) @@ -103,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -111,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -119,7 +123,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -139,54 +143,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -210,7 +167,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -243,11 +200,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.server); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.server); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index 29eb51222..a7791b99a 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +20,7 @@ import ( "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -33,7 +34,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all servers of a project", @@ -57,9 +58,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit server list --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -77,23 +78,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list servers: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No servers found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -105,7 +103,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -125,20 +123,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListServersRequest { - req := apiClient.ListServers(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListServers(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } @@ -147,7 +137,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, servers []iaas.Server) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, servers []iaas.Server) error { switch outputFormat { case print.JSONOutputFormat: details, err := json.MarshalIndent(servers, "", " ") @@ -166,21 +156,25 @@ func outputResult(p *print.Printer, outputFormat string, servers []iaas.Server) return nil default: + if len(servers) == 0 { + p.Outputf("No servers found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() - table.SetHeader("ID", "Name", "Status", "Machine Type", "Availability Zones", "Nic IPv4", "Public IPs") + table.SetHeader("ID", "Name", "Status", "Machine Type", "Availability Zones", "Nic IPv4", "Public IPs", "Agent") for i := range servers { server := servers[i] nicIPv4 := "" publicIPs := "" - if server.Nics != nil && len(*server.Nics) > 0 { - for i, nic := range *server.Nics { + if len(server.Nics) > 0 { + for i, nic := range server.Nics { if nic.Ipv4 != nil || nic.PublicIp != nil { nicIPv4 += utils.PtrString(nic.Ipv4) publicIPs += utils.PtrString(nic.PublicIp) - if i != len(*server.Nics)-1 { + if i != len(server.Nics)-1 { publicIPs += "\n" nicIPv4 += "\n" } @@ -188,14 +182,20 @@ func outputResult(p *print.Printer, outputFormat string, servers []iaas.Server) } } + agent := "" + if server.Agent != nil { + agent = utils.PtrString(server.Agent.Provisioned) + } + table.AddRow( utils.PtrString(server.Id), - utils.PtrString(server.Name), + server.Name, utils.PtrString(server.Status), - utils.PtrString(server.MachineType), + server.MachineType, utils.PtrString(server.AvailabilityZone), nicIPv4, publicIPs, + agent, ) } diff --git a/internal/cmd/server/list/list_test.go b/internal/cmd/server/list/list_test.go index 64fe0ddb4..13eda55f4 100644 --- a/internal/cmd/server/list/list_test.go +++ b/internal/cmd/server/list/list_test.go @@ -4,29 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testLabelSelector = "label" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", labelSelectorFlag: testLabelSelector, } @@ -41,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), @@ -52,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListServersRequest)) iaas.ApiListServersRequest { - request := testClient.ListServers(testCtx, testProjectId) + request := testClient.DefaultAPI.ListServers(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabelSelector) request = request.Details(true) for _, mod := range mods { @@ -64,6 +69,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListServersRequest)) iaas.ApiL func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -87,21 +93,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -133,46 +139,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -196,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -208,6 +175,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string servers []iaas.Server } tests := []struct { @@ -221,11 +189,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.servers); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.servers); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/log/log.go b/internal/cmd/server/log/log.go index 11bbfe006..da9d90a9a 100644 --- a/internal/cmd/server/log/log.go +++ b/internal/cmd/server/log/log.go @@ -2,12 +2,13 @@ package log import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -35,7 +35,7 @@ type inputModel struct { Length *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("log %s", serverIdArg), Short: "Gets server console log", @@ -68,7 +68,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -125,42 +125,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Length: utils.Ptr(length), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetServerLogRequest { - return apiClient.GetServerLog(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.GetServerLog(ctx, model.ProjectId, model.Region, model.ServerId) } func outputResult(p *print.Printer, outputFormat, serverLabel, log string) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(log, "", " ") - if err != nil { - return fmt.Errorf("marshal url: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(log, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal url: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, log, func() error { p.Outputf("Log for server %q\n%s", serverLabel, log) return nil - } + }) } diff --git a/internal/cmd/server/log/log_test.go b/internal/cmd/server/log/log_test.go index 0ebc57bdc..62232e266 100644 --- a/internal/cmd/server/log/log_test.go +++ b/internal/cmd/server/log/log_test.go @@ -4,23 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -36,7 +38,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + lengthLimitFlag: "3000", } for _, mod := range mods { @@ -50,6 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, Length: utils.Ptr(int64(3000)), @@ -61,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetServerLogRequest)) iaas.ApiGetServerLogRequest { - request := testClient.GetServerLog(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.GetServerLog(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -99,7 +104,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -107,7 +112,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -142,54 +147,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -213,7 +171,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -239,11 +197,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.log); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.log); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/machine-type/describe/describe.go b/internal/cmd/server/machine-type/describe/describe.go index 522d13bd3..92ba40cde 100644 --- a/internal/cmd/server/machine-type/describe/describe.go +++ b/internal/cmd/server/machine-type/describe/describe.go @@ -2,12 +2,12 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -30,7 +29,7 @@ type inputModel struct { MachineType string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", machineTypeArg), Short: "Shows details of a server machine type", @@ -85,52 +84,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu MachineType: machineType, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetMachineTypeRequest { - return apiClient.GetMachineType(ctx, model.ProjectId, model.MachineType) + return apiClient.DefaultAPI.GetMachineType(ctx, model.ProjectId, model.Region, model.MachineType) } func outputResult(p *print.Printer, outputFormat string, machineType *iaas.MachineType) error { if machineType == nil { return fmt.Errorf("api response for machine type is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(machineType, "", " ") - if err != nil { - return fmt.Errorf("marshal server machine type: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(machineType, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server machine type: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, machineType, func() error { table := tables.NewTable() - table.AddRow("NAME", utils.PtrString(machineType.Name)) + table.AddRow("NAME", machineType.Name) table.AddSeparator() - table.AddRow("VCPUS", utils.PtrString(machineType.Vcpus)) + table.AddRow("VCPUS", machineType.Vcpus) table.AddSeparator() - table.AddRow("RAM (in MB)", utils.PtrString(machineType.Ram)) + table.AddRow("RAM (in MB)", machineType.Ram) table.AddSeparator() - table.AddRow("DISK SIZE (in GB)", utils.PtrString(machineType.Disk)) + table.AddRow("DISK SIZE (in GB)", machineType.Disk) table.AddSeparator() table.AddRow("DESCRIPTION", utils.PtrString(machineType.Description)) table.AddSeparator() @@ -140,5 +114,5 @@ func outputResult(p *print.Printer, outputFormat string, machineType *iaas.Machi return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/machine-type/describe/describe_test.go b/internal/cmd/server/machine-type/describe/describe_test.go index 20e47affc..01f52cf5a 100644 --- a/internal/cmd/server/machine-type/describe/describe_test.go +++ b/internal/cmd/server/machine-type/describe/describe_test.go @@ -4,22 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testMachineType = "t1.1" @@ -35,7 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, MachineType: testMachineType, @@ -58,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetMachineTypeRequest)) iaas.ApiGetMachineTypeRequest { - request := testClient.GetMachineType(testCtx, testProjectId, testMachineType) + request := testClient.DefaultAPI.GetMachineType(testCtx, testProjectId, testRegion, testMachineType) for _, mod := range mods { mod(&request) } @@ -102,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -126,54 +130,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -197,7 +154,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -222,11 +179,10 @@ func TestOutputResult(t *testing.T) { wantErr: true, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.machineType); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.machineType); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/machine-type/list/list.go b/internal/cmd/server/machine-type/list/list.go index 5ec90d0c4..0f5cd707c 100644 --- a/internal/cmd/server/machine-type/list/list.go +++ b/internal/cmd/server/machine-type/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,19 +18,21 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) type inputModel struct { *globalflags.GlobalFlagModel - Limit *int64 + Limit *int64 + Filter *string } const ( - limitFlag = "limit" + limitFlag = "limit" + filterFlag = "filter" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Get list of all machine types available in a project", @@ -50,10 +51,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List the first 10 machine types`, `$ stackit server machine-type list --limit=10`, ), + examples.NewExample( + `List machine types with exactly 2 vCPUs`, + `$ stackit server machine-type list --filter="vcpus==2"`, + ), + examples.NewExample( + `List machine types with at least 2 vCPUs and 2048 MB RAM`, + `$ stackit server machine-type list --filter="vcpus >= 2 && ram >= 2048"`, + ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -71,22 +80,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read machine-types: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No machine-types found for project %q\n", projectLabel) - return nil + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // limit output - if model.Limit != nil && len(*resp.Items) > int(*model.Limit) { - *resp.Items = (*resp.Items)[:*model.Limit] + if model.Limit != nil && len(resp.Items) > int(*model.Limit) { + resp.Items = (resp.Items)[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, *resp) + return outputResult(params.Printer, model.OutputFormat, projectLabel, *resp) }, } @@ -96,9 +101,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements") + cmd.Flags().String(filterFlag, "", "Filter resources by fields. A subset of expr-lang is supported. See https://expr-lang.org/docs/language-definition for usage details") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -114,51 +120,49 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Limit: limit, + Filter: flags.FlagToStringPointer(p, cmd, filterFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListMachineTypesRequest { - return apiClient.ListMachineTypes(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListMachineTypes(ctx, model.ProjectId, model.Region) + if model.Filter != nil { + req = req.Filter(*model.Filter) + } + return req } -func outputResult(p *print.Printer, outputFormat string, machineTypes iaas.MachineTypeListResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(machineTypes, "", " ") - if err != nil { - return fmt.Errorf("marshal machineTypes: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(machineTypes, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal machineTypes: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, machineTypes iaas.MachineTypeListResponse) error { + return p.OutputResult(outputFormat, machineTypes, func() error { + if len(machineTypes.Items) == 0 { + p.Outputf("No machine-types found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetTitle("Machine-Types") - - table.SetHeader("NAME", "DESCRIPTION") + table.SetHeader("NAME", "VCPUS", "RAM (GB)", "DESCRIPTION", "EXTRA SPECS") if items := machineTypes.GetItems(); len(items) > 0 { for _, machineType := range items { - table.AddRow(*machineType.Name, utils.PtrString(machineType.Description)) + extraSpecMap := make(map[string]string) + if len(machineType.ExtraSpecs) > 0 { + for key, value := range machineType.ExtraSpecs { + extraSpecMap[key] = fmt.Sprintf("%v", value) + } + } + ramGB := machineType.Ram / 1024 + + table.AddRow( + machineType.Name, + machineType.Vcpus, + ramGB, + utils.PtrString(machineType.Description), + utils.JoinStringMap(extraSpecMap, ": ", "\n"), + ) + table.AddSeparator() } } @@ -168,5 +172,5 @@ func outputResult(p *print.Printer, outputFormat string, machineTypes iaas.Machi } return nil - } + }) } diff --git a/internal/cmd/server/machine-type/list/list_test.go b/internal/cmd/server/machine-type/list/list_test.go index 8472b596d..4f1b707cc 100644 --- a/internal/cmd/server/machine-type/list/list_test.go +++ b/internal/cmd/server/machine-type/list/list_test.go @@ -4,29 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -39,6 +43,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), } @@ -49,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListMachineTypesRequest)) iaas.ApiListMachineTypesRequest { - request := testClient.ListMachineTypes(testCtx, testProjectId) + request := testClient.DefaultAPI.ListMachineTypes(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -59,6 +64,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListMachineTypesRequest)) iaas func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -69,6 +75,16 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(), }, + { + description: "with filter", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[filterFlag] = "vcpus >= 2 && ram >= 2048" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Filter = utils.Ptr("vcpus >= 2 && ram >= 2048") + }), + }, { description: "no values", flagValues: map[string]string{}, @@ -82,21 +98,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -118,46 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -173,6 +150,15 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(), expectedRequest: fixtureRequest(), }, + { + description: "with filter", + model: fixtureInputModel(func(model *inputModel) { + model.Filter = utils.Ptr("vcpus==2") + }), + expectedRequest: fixtureRequest(func(request *iaas.ApiListMachineTypesRequest) { + *request = request.Filter("vcpus==2") + }), + }, } for _, tt := range tests { @@ -181,7 +167,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -193,6 +179,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string machineTypes iaas.MachineTypeListResponse } tests := []struct { @@ -205,12 +192,38 @@ func TestOutputResult(t *testing.T) { args: args{}, wantErr: false, }, + { + name: "with populated data", + args: args{ + outputFormat: "table", + machineTypes: iaas.MachineTypeListResponse{ + Items: []iaas.MachineType{ + { + Name: "c1.2", + Vcpus: int64(2), + Ram: int64(2048), // Should display as 2 GB + Description: utils.Ptr("Compute optimized 2 vCPU"), + ExtraSpecs: map[string]interface{}{ + "cpu": "intel-icelake-generic", + }, + }, + { + Name: "m1.2", + Vcpus: int64(2), + Ram: int64(8192), // Should display as 8 GB + Description: utils.Ptr("Memory optimized 2 vCPU"), + // No ExtraSpecs provided to test nil safety + }, + }, + }, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.machineTypes); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.machineTypes); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/machine-type/machine-type.go b/internal/cmd/server/machine-type/machine-type.go index 9482a6143..ee4e2ae54 100644 --- a/internal/cmd/server/machine-type/machine-type.go +++ b/internal/cmd/server/machine-type/machine-type.go @@ -1,16 +1,16 @@ package machinetype import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/machine-type/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/machine-type/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "machine-type", Short: "Provides functionality for server machine types available inside a project", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) } diff --git a/internal/cmd/server/network-interface/attach/attach.go b/internal/cmd/server/network-interface/attach/attach.go index 6f2a64163..582dfbb7c 100644 --- a/internal/cmd/server/network-interface/attach/attach.go +++ b/internal/cmd/server/network-interface/attach/attach.go @@ -4,10 +4,12 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -16,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -36,7 +37,7 @@ type inputModel struct { Create *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "attach", Short: "Attaches a network interface to a server", @@ -52,9 +53,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit server network-interface attach --network-id xxx --server-id yyy --create`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -65,7 +66,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = *model.ServerId @@ -73,18 +74,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // if the create flag is provided a network interface will be created and attached if model.Create != nil && *model.Create { - networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, *model.NetworkId) + networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.NetworkId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) networkLabel = *model.NetworkId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a network interface for network %q and attach it to server %q?", networkLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a network interface for network %q and attach it to server %q?", networkLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } + // Call API req := buildRequestCreateAndAttach(ctx, model, apiClient) err = req.Execute() @@ -95,13 +95,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return nil } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to attach network interface %q to server %q?", *model.NicId, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to attach network interface %q to server %q?", *model.NicId, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } + // Call API req := buildRequestAttach(ctx, model, apiClient) err = req.Execute() @@ -131,10 +130,10 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { - return nil, &errors.ProjectIdError{} + return nil, &cliErr.ProjectIdError{} } // if create is not provided then network-interface-id is needed @@ -152,22 +151,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Create: create, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequestAttach(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddNicToServerRequest { - return apiClient.AddNicToServer(ctx, model.ProjectId, *model.ServerId, *model.NicId) + return apiClient.DefaultAPI.AddNicToServer(ctx, model.ProjectId, model.Region, *model.ServerId, *model.NicId) } func buildRequestCreateAndAttach(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddNetworkToServerRequest { - return apiClient.AddNetworkToServer(ctx, model.ProjectId, *model.ServerId, *model.NetworkId) + return apiClient.DefaultAPI.AddNetworkToServer(ctx, model.ProjectId, model.Region, *model.ServerId, *model.NetworkId) } diff --git a/internal/cmd/server/network-interface/attach/attach_test.go b/internal/cmd/server/network-interface/attach/attach_test.go index 6e5a00011..89e90b462 100644 --- a/internal/cmd/server/network-interface/attach/attach_test.go +++ b/internal/cmd/server/network-interface/attach/attach_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testNicId = uuid.NewString() @@ -29,7 +30,9 @@ var testNetworkId = uuid.NewString() // contains nic id func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, networkInterfaceIdFlag: testNicId, } @@ -44,6 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: utils.Ptr(testServerId), NicId: utils.Ptr(testNicId), @@ -55,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequestAttach(mods ...func(request *iaas.ApiAddNicToServerRequest)) iaas.ApiAddNicToServerRequest { - request := testClient.AddNicToServer(testCtx, testProjectId, testServerId, testNicId) + request := testClient.DefaultAPI.AddNicToServer(testCtx, testProjectId, testRegion, testServerId, testNicId) for _, mod := range mods { mod(&request) } @@ -63,7 +67,7 @@ func fixtureRequestAttach(mods ...func(request *iaas.ApiAddNicToServerRequest)) } func fixtureRequestCreateAndAttach(mods ...func(request *iaas.ApiAddNetworkToServerRequest)) iaas.ApiAddNetworkToServerRequest { - request := testClient.AddNetworkToServer(testCtx, testProjectId, testServerId, testNetworkId) + request := testClient.DefaultAPI.AddNetworkToServer(testCtx, testProjectId, testRegion, testServerId, testNetworkId) for _, mod := range mods { mod(&request) } @@ -73,6 +77,7 @@ func fixtureRequestCreateAndAttach(mods ...func(request *iaas.ApiAddNetworkToSer func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -91,21 +96,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -199,54 +204,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flag groups: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -270,7 +228,7 @@ func TestBuildRequestAttach(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -302,7 +260,7 @@ func TestBuildRequestCreateAndAttach(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/network-interface/detach/detach.go b/internal/cmd/server/network-interface/detach/detach.go index 58024dc3e..96123ed72 100644 --- a/internal/cmd/server/network-interface/detach/detach.go +++ b/internal/cmd/server/network-interface/detach/detach.go @@ -4,10 +4,12 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -16,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -36,7 +37,7 @@ type inputModel struct { Delete *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "detach", Short: "Detaches a network interface from a server", @@ -52,9 +53,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit server network-interface detach --network-id xxx --server-id yyy --delete`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -65,7 +66,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = *model.ServerId @@ -75,18 +76,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // if the delete flag is provided a network interface is detached and deleted if model.Delete != nil && *model.Delete { - networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, *model.NetworkId) + networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *model.NetworkId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) networkLabel = *model.NetworkId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to detach and delete all network interfaces of network %q from server %q? (This cannot be undone)", networkLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to detach and delete all network interfaces of network %q from server %q? (This cannot be undone)", networkLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } + // Call API req := buildRequestDetachAndDelete(ctx, model, apiClient) err = req.Execute() @@ -97,13 +97,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return nil } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to detach network interface %q from server %q?", *model.NicId, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to detach network interface %q from server %q?", *model.NicId, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } + // Call API req := buildRequestDetach(ctx, model, apiClient) err = req.Execute() @@ -133,10 +132,10 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { - return nil, &errors.ProjectIdError{} + return nil, &cliErr.ProjectIdError{} } // if delete is not provided then network-interface-id is needed @@ -154,22 +153,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Delete: deleteValue, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequestDetach(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveNicFromServerRequest { - return apiClient.RemoveNicFromServer(ctx, model.ProjectId, *model.ServerId, *model.NicId) + return apiClient.DefaultAPI.RemoveNicFromServer(ctx, model.ProjectId, model.Region, *model.ServerId, *model.NicId) } func buildRequestDetachAndDelete(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveNetworkFromServerRequest { - return apiClient.RemoveNetworkFromServer(ctx, model.ProjectId, *model.ServerId, *model.NetworkId) + return apiClient.DefaultAPI.RemoveNetworkFromServer(ctx, model.ProjectId, model.Region, *model.ServerId, *model.NetworkId) } diff --git a/internal/cmd/server/network-interface/detach/detach_test.go b/internal/cmd/server/network-interface/detach/detach_test.go index cc60c9912..5c2782a19 100644 --- a/internal/cmd/server/network-interface/detach/detach_test.go +++ b/internal/cmd/server/network-interface/detach/detach_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testNicId = uuid.NewString() @@ -29,7 +30,9 @@ var testNetworkId = uuid.NewString() // contains nic id func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, networkInterfaceIdFlag: testNicId, } @@ -44,6 +47,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: utils.Ptr(testServerId), NicId: utils.Ptr(testNicId), @@ -55,7 +59,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequestDetach(mods ...func(request *iaas.ApiRemoveNicFromServerRequest)) iaas.ApiRemoveNicFromServerRequest { - request := testClient.RemoveNicFromServer(testCtx, testProjectId, testServerId, testNicId) + request := testClient.DefaultAPI.RemoveNicFromServer(testCtx, testProjectId, testRegion, testServerId, testNicId) for _, mod := range mods { mod(&request) } @@ -63,7 +67,7 @@ func fixtureRequestDetach(mods ...func(request *iaas.ApiRemoveNicFromServerReque } func fixtureRequestDetachAndDelete(mods ...func(request *iaas.ApiRemoveNetworkFromServerRequest)) iaas.ApiRemoveNetworkFromServerRequest { - request := testClient.RemoveNetworkFromServer(testCtx, testProjectId, testServerId, testNetworkId) + request := testClient.DefaultAPI.RemoveNetworkFromServer(testCtx, testProjectId, testRegion, testServerId, testNetworkId) for _, mod := range mods { mod(&request) } @@ -73,6 +77,7 @@ func fixtureRequestDetachAndDelete(mods ...func(request *iaas.ApiRemoveNetworkFr func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -91,21 +96,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -199,54 +204,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flag groups: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -270,7 +228,7 @@ func TestBuildRequestDetach(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -302,7 +260,7 @@ func TestBuildRequestDetachAndDelete(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/network-interface/list/list.go b/internal/cmd/server/network-interface/list/list.go index c3d9769c3..4fa16c117 100644 --- a/internal/cmd/server/network-interface/list/list.go +++ b/internal/cmd/server/network-interface/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -28,11 +28,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all attached network interfaces of a server", @@ -52,9 +52,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit server network-interface list --server-id xxx --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,25 +72,22 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list attached network interfaces: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId - } else if serverLabel == "" { - serverLabel = *model.ServerId - } - params.Printer.Info("No attached network interfaces found for server %q\n", serverLabel) - return nil + items := resp.GetItems() + + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) + serverLabel = model.ServerId + } else if serverLabel == "" { + serverLabel = model.ServerId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, *model.ServerId, items) + return outputResult(params.Printer, model.OutputFormat, model.ServerId, serverLabel, items) }, } configureFlags(cmd) @@ -105,7 +102,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -121,45 +118,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListServerNicsRequest { - return apiClient.ListServerNics(ctx, model.ProjectId, *model.ServerId) +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListServerNICsRequest { + return apiClient.DefaultAPI.ListServerNICs(ctx, model.ProjectId, model.Region, model.ServerId) } -func outputResult(p *print.Printer, outputFormat, serverId string, serverNics []iaas.NIC) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(serverNics, "", " ") - if err != nil { - return fmt.Errorf("marshal server network interfaces: %w", err) +func outputResult(p *print.Printer, outputFormat, serverId, serverLabel string, serverNics []iaas.NIC) error { + return p.OutputResult(outputFormat, serverNics, func() error { + if len(serverNics) == 0 { + p.Outputf("No attached network interfaces found for server %q\n", serverLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(serverNics, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server network interfaces: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("NIC ID", "SERVER ID") @@ -171,5 +147,5 @@ func outputResult(p *print.Printer, outputFormat, serverId string, serverNics [] p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/server/network-interface/list/list_test.go b/internal/cmd/server/network-interface/list/list_test.go index 8d2c90f86..424a0f254 100644 --- a/internal/cmd/server/network-interface/list/list_test.go +++ b/internal/cmd/server/network-interface/list/list_test.go @@ -4,31 +4,35 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - limitFlag: "10", - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + limitFlag: "10", + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -41,9 +45,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, } for _, mod := range mods { mod(model) @@ -51,8 +56,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *iaas.ApiListServerNicsRequest)) iaas.ApiListServerNicsRequest { - request := testClient.ListServerNics(testCtx, testProjectId, testServerId) +func fixtureRequest(mods ...func(request *iaas.ApiListServerNICsRequest)) iaas.ApiListServerNICsRequest { + request := testClient.DefaultAPI.ListServerNICs(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -62,6 +67,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListServerNicsRequest)) iaas.A func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -80,21 +86,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,46 +143,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -185,7 +152,7 @@ func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest iaas.ApiListServerNicsRequest + expectedRequest iaas.ApiListServerNICsRequest }{ { description: "base", @@ -200,7 +167,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -213,6 +180,7 @@ func TestOutputResult(t *testing.T) { type args struct { outputFormat string serverId string + serverLabel string serverNics []iaas.NIC } tests := []struct { @@ -235,11 +203,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverId, tt.args.serverNics); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverId, tt.args.serverLabel, tt.args.serverNics); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/network-interface/network-interface.go b/internal/cmd/server/network-interface/network-interface.go index 998ee07d3..2496def12 100644 --- a/internal/cmd/server/network-interface/network-interface.go +++ b/internal/cmd/server/network-interface/network-interface.go @@ -1,17 +1,17 @@ package networkinterface import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/network-interface/attach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/network-interface/detach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/network-interface/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "network-interface", Short: "Allows attaching/detaching network interfaces to servers", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(attach.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(detach.NewCmd(params)) diff --git a/internal/cmd/server/os-update/create/create.go b/internal/cmd/server/os-update/create/create.go index 76d97cc01..a41e2da18 100644 --- a/internal/cmd/server/os-update/create/create.go +++ b/internal/cmd/server/os-update/create/create.go @@ -2,13 +2,15 @@ package create import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/spf13/cobra" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,10 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) const ( @@ -33,10 +31,10 @@ type inputModel struct { *globalflags.GlobalFlagModel ServerId string - MaintenanceWindow int64 + MaintenanceWindow int32 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Server os-update.", @@ -50,10 +48,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a Server os-update with name "myupdate" and maintenance window for 13 o'clock.`, `$ stackit server os-update create --server-id xxx --maintenance-window=13`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,7 +65,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -75,12 +73,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a os-update for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a os-update for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -93,7 +89,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("create Server os-update: %w", err) } - return outputResult(params.Printer, model.OutputFormat, serverLabel, *resp) + return outputResult(params.Printer, model.OutputFormat, serverLabel, resp) }, } configureFlags(cmd) @@ -102,10 +98,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") - cmd.Flags().Int64P(maintenanceWindowFlag, "m", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)") + cmd.Flags().Int32P(maintenanceWindowFlag, "m", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -114,50 +110,29 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), - MaintenanceWindow: flags.FlagWithDefaultToInt64Value(p, cmd, maintenanceWindowFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + MaintenanceWindow: flags.FlagWithDefaultToInt32Value(p, cmd, maintenanceWindowFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) (serverupdate.ApiCreateUpdateRequest, error) { - req := apiClient.CreateUpdate(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.CreateUpdate(ctx, model.ProjectId, model.ServerId, model.Region) payload := serverupdate.CreateUpdatePayload{ - MaintenanceWindow: &model.MaintenanceWindow, + MaintenanceWindow: model.MaintenanceWindow, } req = req.CreateUpdatePayload(payload) return req, nil } -func outputResult(p *print.Printer, outputFormat, serverLabel string, resp serverupdate.Update) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal server os-update: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server os-update: %w", err) +func outputResult(p *print.Printer, outputFormat, serverLabel string, resp *serverupdate.Update) error { + return p.OutputResult(outputFormat, resp, func() error { + if resp == nil { + return fmt.Errorf("response is nil") } - p.Outputln(string(details)) + p.Outputf("Triggered creation of server os-update for server %s. Update ID: %d\n", serverLabel, resp.Id) return nil - default: - p.Outputf("Triggered creation of server os-update for server %s. Update ID: %s\n", serverLabel, utils.PtrString(resp.Id)) - return nil - } + }) } diff --git a/internal/cmd/server/os-update/create/create_test.go b/internal/cmd/server/os-update/create/create_test.go index 78e914882..fd0cdf2ff 100644 --- a/internal/cmd/server/os-update/create/create_test.go +++ b/internal/cmd/server/os-update/create/create_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -48,7 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, ServerId: testServerId, - MaintenanceWindow: int64(13), + MaintenanceWindow: int32(13), } for _, mod := range mods { mod(model) @@ -57,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateRequest)) serverupdate.ApiCreateUpdateRequest { - request := testClient.CreateUpdate(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.CreateUpdate(testCtx, testProjectId, testServerId, testRegion) request = request.CreateUpdatePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -67,7 +67,7 @@ func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateRequest)) func fixturePayload(mods ...func(payload *serverupdate.CreateUpdatePayload)) serverupdate.CreateUpdatePayload { payload := serverupdate.CreateUpdatePayload{ - MaintenanceWindow: utils.Ptr(int64(13)), + MaintenanceWindow: int32(13), } for _, mod := range mods { mod(&payload) @@ -78,6 +78,7 @@ func fixturePayload(mods ...func(payload *serverupdate.CreateUpdatePayload)) ser func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -129,46 +130,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -200,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -213,7 +175,7 @@ func TestOutputResult(t *testing.T) { type args struct { outputFormat string serverLabel string - resp serverupdate.Update + resp *serverupdate.Update } tests := []struct { name string @@ -221,16 +183,26 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + resp: &serverupdate.Update{}, + }, wantErr: false, }, + { + name: "nil", + args: args{ + outputFormat: print.PrettyOutputFormat, + resp: nil, + }, + wantErr: true, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/os-update/describe/describe.go b/internal/cmd/server/os-update/describe/describe.go index 3c8de58ee..f1550583e 100644 --- a/internal/cmd/server/os-update/describe/describe.go +++ b/internal/cmd/server/os-update/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) const ( @@ -31,7 +31,7 @@ type inputModel struct { UpdateId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", updateIdArg), Short: "Shows details of a Server os-update", @@ -64,7 +64,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read server os-update: %w", err) } - return outputResult(params.Printer, model.OutputFormat, *resp) + return outputResult(params.Printer, model.OutputFormat, resp) }, } configureFlags(cmd) @@ -92,46 +92,25 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu UpdateId: updateId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiGetUpdateRequest { - req := apiClient.GetUpdate(ctx, model.ProjectId, model.ServerId, model.UpdateId, model.Region) + req := apiClient.DefaultAPI.GetUpdate(ctx, model.ProjectId, model.ServerId, model.UpdateId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, update serverupdate.Update) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(update, "", " ") - if err != nil { - return fmt.Errorf("marshal server update: %w", err) +func outputResult(p *print.Printer, outputFormat string, update *serverupdate.Update) error { + return p.OutputResult(outputFormat, update, func() error { + if update == nil { + return fmt.Errorf("update is nil") } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(update, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server update: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() - table.AddRow("ID", utils.PtrString(update.Id)) + table.AddRow("ID", update.Id) table.AddSeparator() - table.AddRow("STATUS", utils.PtrString(update.Status)) + table.AddRow("STATUS", update.Status) table.AddSeparator() installedUpdates := utils.PtrStringDefault(update.InstalledUpdates, "n/a") table.AddRow("INSTALLED UPDATES", installedUpdates) @@ -139,7 +118,7 @@ func outputResult(p *print.Printer, outputFormat string, update serverupdate.Upd failedUpdates := utils.PtrStringDefault(update.FailedUpdates, "n/a") table.AddRow("FAILED UPDATES", failedUpdates) - table.AddRow("START DATE", utils.PtrString(update.StartDate)) + table.AddRow("START DATE", update.StartDate) table.AddSeparator() table.AddRow("END DATE", utils.PtrString(update.EndDate)) table.AddSeparator() @@ -150,5 +129,5 @@ func outputResult(p *print.Printer, outputFormat string, update serverupdate.Upd } return nil - } + }) } diff --git a/internal/cmd/server/os-update/describe/describe_test.go b/internal/cmd/server/os-update/describe/describe_test.go index 0a859c415..e3c84be5c 100644 --- a/internal/cmd/server/os-update/describe/describe_test.go +++ b/internal/cmd/server/os-update/describe/describe_test.go @@ -4,14 +4,16 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) const ( @@ -21,7 +23,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testUpdateId = uuid.NewString() @@ -65,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiGetUpdateRequest)) serverupdate.ApiGetUpdateRequest { - request := testClient.GetUpdate(testCtx, testProjectId, testServerId, testUpdateId, testRegion) + request := testClient.DefaultAPI.GetUpdate(testCtx, testProjectId, testServerId, testUpdateId, testRegion) for _, mod := range mods { mod(&request) } @@ -133,54 +135,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -206,7 +161,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -218,7 +173,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string - update serverupdate.Update + update *serverupdate.Update } tests := []struct { name string @@ -226,16 +181,26 @@ func TestOutputResult(t *testing.T) { wantErr bool }{ { - name: "empty", - args: args{}, + name: "empty", + args: args{ + outputFormat: print.PrettyOutputFormat, + update: &serverupdate.Update{}, + }, wantErr: false, }, + { + name: "nil", + args: args{ + outputFormat: print.PrettyOutputFormat, + update: nil, + }, + wantErr: true, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.update); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.update); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/os-update/disable/disable.go b/internal/cmd/server/os-update/disable/disable.go index 510411798..9766e3526 100644 --- a/internal/cmd/server/os-update/disable/disable.go +++ b/internal/cmd/server/os-update/disable/disable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -28,7 +29,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "disable", Short: "Disables server os-update service", @@ -39,9 +40,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Disable os-update functionality for your server.`, "$ stackit server os-update disable --server-id=zzz"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -55,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -63,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disable the os-update service for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disable the os-update service for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -93,7 +92,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -104,19 +103,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiDisableServiceResourceRequest { - req := apiClient.DisableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.DisableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region) return req } diff --git a/internal/cmd/server/os-update/disable/disable_test.go b/internal/cmd/server/os-update/disable/disable_test.go index 566300e33..7726b10e6 100644 --- a/internal/cmd/server/os-update/disable/disable_test.go +++ b/internal/cmd/server/os-update/disable/disable_test.go @@ -5,13 +5,12 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -21,7 +20,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -29,6 +28,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -52,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiDisableServiceResourceRequest)) serverupdate.ApiDisableServiceResourceRequest { - request := testClient.DisableServiceResource(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.DisableServiceResource(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -62,17 +62,23 @@ func fixtureRequest(mods ...func(request *serverupdate.ApiDisableServiceResource func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel }{ { - description: "base", - flagValues: fixtureFlagValues(), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.ServerId = "" + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "server id flag is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "" }), + isValid: false, }, { description: "no values", @@ -104,46 +110,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -167,7 +134,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/os-update/enable/enable.go b/internal/cmd/server/os-update/enable/enable.go index e796767c6..779a6150f 100644 --- a/internal/cmd/server/os-update/enable/enable.go +++ b/internal/cmd/server/os-update/enable/enable.go @@ -5,7 +5,8 @@ import ( "fmt" "strings" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -29,7 +30,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "enable", Short: "Enables Server os-update service", @@ -40,9 +41,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Enable os-update functionality for your server`, "$ stackit server os-update enable --server-id=zzz"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -56,7 +57,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -64,12 +65,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to enable the server os-update service for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to enable the server os-update service for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -96,7 +95,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -107,20 +106,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiEnableServiceResourceRequest { payload := serverupdate.EnableServiceResourcePayload{} - req := apiClient.EnableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region).EnableServiceResourcePayload(payload) + req := apiClient.DefaultAPI.EnableServiceResource(ctx, model.ProjectId, model.ServerId, model.Region).EnableServiceResourcePayload(payload) return req } diff --git a/internal/cmd/server/os-update/enable/enable_test.go b/internal/cmd/server/os-update/enable/enable_test.go index d35251a70..94d9f1ce9 100644 --- a/internal/cmd/server/os-update/enable/enable_test.go +++ b/internal/cmd/server/os-update/enable/enable_test.go @@ -5,13 +5,12 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -21,7 +20,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -29,6 +28,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -52,7 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiEnableServiceResourceRequest)) serverupdate.ApiEnableServiceResourceRequest { - request := testClient.EnableServiceResource(testCtx, testProjectId, testServerId, testRegion).EnableServiceResourcePayload(serverupdate.EnableServiceResourcePayload{}) + request := testClient.DefaultAPI.EnableServiceResource(testCtx, testProjectId, testServerId, testRegion).EnableServiceResourcePayload(serverupdate.EnableServiceResourcePayload{}) for _, mod := range mods { mod(&request) } @@ -62,17 +62,23 @@ func fixtureRequest(mods ...func(request *serverupdate.ApiEnableServiceResourceR func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel }{ { - description: "base", - flagValues: fixtureFlagValues(), - isValid: true, - expectedModel: fixtureInputModel(func(model *inputModel) { - model.ServerId = "" + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "server id flag is missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "" }), + isValid: false, }, { description: "no values", @@ -104,46 +110,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -167,7 +134,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/os-update/list/list.go b/internal/cmd/server/os-update/list/list.go index 4092be38a..19ef3ba2f 100644 --- a/internal/cmd/server/os-update/list/list.go +++ b/internal/cmd/server/os-update/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "strconv" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,9 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -35,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server os-updates", @@ -49,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all os-updates for a server with ID "xxx" in JSON format`, "$ stackit server os-update list --server-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,27 +66,24 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server os-update: %w", err) } - updates := *resp.Items - if len(updates) == 0 { - serverLabel := model.ServerId - // Get server name - if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - } else if serverName != "" { - serverLabel = serverName - } + updates := resp.GetItems() + + serverLabel := model.ServerId + // Get server name + if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) + } else if serverName != "" { + serverLabel = serverName } - params.Printer.Info("No os-updates found for server %s\n", serverLabel) - return nil } // Truncate output if model.Limit != nil && len(updates) > int(*model.Limit) { updates = updates[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, updates) + return outputResult(params.Printer, model.OutputFormat, serverLabel, updates) }, } configureFlags(cmd) @@ -103,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,42 +118,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiListUpdatesRequest { - req := apiClient.ListUpdates(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.ListUpdates(ctx, model.ProjectId, model.ServerId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, updates []serverupdate.Update) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(updates, "", " ") - if err != nil { - return fmt.Errorf("marshal server os-update list: %w", err) +func outputResult(p *print.Printer, outputFormat, serverLabel string, updates []serverupdate.Update) error { + return p.OutputResult(outputFormat, updates, func() error { + if len(updates) == 0 { + p.Outputf("No os-updates found for server %s\n", serverLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(updates, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server os-update list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "STATUS", "INSTALLED UPDATES", "FAILED UPDATES", "START DATE", "END DATE") for i := range updates { @@ -168,20 +142,20 @@ func outputResult(p *print.Printer, outputFormat string, updates []serverupdate. installed := "n/a" if s.InstalledUpdates != nil { - installed = strconv.FormatInt(*s.InstalledUpdates, 10) + installed = utils.PtrString(s.InstalledUpdates) } failed := "n/a" if s.FailedUpdates != nil { - failed = strconv.FormatInt(*s.FailedUpdates, 10) + failed = utils.PtrString(s.FailedUpdates) } table.AddRow( - utils.PtrString(s.Id), - utils.PtrString(s.Status), + s.Id, + s.Status, installed, failed, - utils.PtrString(s.StartDate), + s.StartDate, endDate, ) } @@ -190,5 +164,5 @@ func outputResult(p *print.Printer, outputFormat string, updates []serverupdate. return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/os-update/list/list_test.go b/internal/cmd/server/os-update/list/list_test.go index 078f7ca0a..b91621d4e 100644 --- a/internal/cmd/server/os-update/list/list_test.go +++ b/internal/cmd/server/os-update/list/list_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -56,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdatesRequest)) serverupdate.ApiListUpdatesRequest { - request := testClient.ListUpdates(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.ListUpdates(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -66,6 +66,7 @@ func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdatesRequest)) s func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,46 +121,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -183,7 +145,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -195,6 +157,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + serverLabel string updates []serverupdate.Update } tests := []struct { @@ -208,11 +171,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.updates); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.updates); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/os-update/os-update.go b/internal/cmd/server/os-update/os-update.go index 516d7ce06..53abb7893 100644 --- a/internal/cmd/server/os-update/os-update.go +++ b/internal/cmd/server/os-update/os-update.go @@ -1,7 +1,6 @@ package osupdate import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/create" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/disable" @@ -9,12 +8,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/list" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/schedule" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "os-update", Short: "Provides functionality for managed server updates", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) diff --git a/internal/cmd/server/os-update/schedule/create/create.go b/internal/cmd/server/os-update/schedule/create/create.go index cae0f7377..d58deb706 100644 --- a/internal/cmd/server/os-update/schedule/create/create.go +++ b/internal/cmd/server/os-update/schedule/create/create.go @@ -2,11 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,10 +18,6 @@ import ( iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) const ( @@ -41,10 +39,10 @@ type inputModel struct { ScheduleName string Enabled bool Rrule string - MaintenanceWindow int64 + MaintenanceWindow int32 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a Server os-update Schedule", @@ -58,10 +56,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a Server os-update Schedule with name "myschedule" and maintenance window for 14 o'clock`, `$ stackit server os-update schedule create --server-id xxx --name=myschedule --maintenance-window=14`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -75,7 +73,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel := model.ServerId // Get server name if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) } else if serverName != "" { @@ -83,12 +81,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a os-update Schedule for server %s?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a os-update Schedule for server %s?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,7 +107,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") cmd.Flags().StringP(nameFlag, "n", "", "os-update schedule name") - cmd.Flags().Int64P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "os-update maintenance window (in hours, 1-24)") + cmd.Flags().Int32P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "os-update maintenance window (in hours, 1-24)") cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server os-update schedule enabled") cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "os-update RRULE (recurrence rule)") @@ -119,7 +115,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -128,55 +124,30 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), - MaintenanceWindow: flags.FlagWithDefaultToInt64Value(p, cmd, maintenanceWindowFlag), + MaintenanceWindow: flags.FlagWithDefaultToInt32Value(p, cmd, maintenanceWindowFlag), ScheduleName: flags.FlagToStringValue(p, cmd, nameFlag), Rrule: flags.FlagWithDefaultToStringValue(p, cmd, rruleFlag), Enabled: flags.FlagToBoolValue(p, cmd, enabledFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) (serverupdate.ApiCreateUpdateScheduleRequest, error) { - req := apiClient.CreateUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.CreateUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.Region) req = req.CreateUpdateSchedulePayload(serverupdate.CreateUpdateSchedulePayload{ - Enabled: &model.Enabled, - Name: &model.ScheduleName, - Rrule: &model.Rrule, - MaintenanceWindow: &model.MaintenanceWindow, + Enabled: model.Enabled, + Name: model.ScheduleName, + Rrule: model.Rrule, + MaintenanceWindow: model.MaintenanceWindow, }) return req, nil } func outputResult(p *print.Printer, outputFormat, serverLabel string, resp serverupdate.UpdateSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal server os-update schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server os-update schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created server os-update schedule for server %s. os-update Schedule ID: %s\n", serverLabel, utils.PtrString(resp.Id)) + return p.OutputResult(outputFormat, resp, func() error { + p.Outputf("Created server os-update schedule for server %s. os-update Schedule ID: %d\n", serverLabel, resp.Id) return nil - } + }) } diff --git a/internal/cmd/server/os-update/schedule/create/create_test.go b/internal/cmd/server/os-update/schedule/create/create_test.go index 2cdb00f75..3e046359d 100644 --- a/internal/cmd/server/os-update/schedule/create/create_test.go +++ b/internal/cmd/server/os-update/schedule/create/create_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) const ( @@ -22,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -54,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ScheduleName: "example-schedule-name", Enabled: defaultEnabled, Rrule: defaultRrule, - MaintenanceWindow: int64(23), + MaintenanceWindow: int32(23), } for _, mod := range mods { mod(model) @@ -63,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateScheduleRequest)) serverupdate.ApiCreateUpdateScheduleRequest { - request := testClient.CreateUpdateSchedule(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.CreateUpdateSchedule(testCtx, testProjectId, testServerId, testRegion) request = request.CreateUpdateSchedulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -73,10 +72,10 @@ func fixtureRequest(mods ...func(request *serverupdate.ApiCreateUpdateScheduleRe func fixturePayload(mods ...func(payload *serverupdate.CreateUpdateSchedulePayload)) serverupdate.CreateUpdateSchedulePayload { payload := serverupdate.CreateUpdateSchedulePayload{ - Name: utils.Ptr("example-schedule-name"), - Enabled: utils.Ptr(defaultEnabled), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: utils.Ptr(int64(23)), + Name: "example-schedule-name", + Enabled: defaultEnabled, + Rrule: "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1", + MaintenanceWindow: int32(23), } for _, mod := range mods { mod(&payload) @@ -87,6 +86,7 @@ func fixturePayload(mods ...func(payload *serverupdate.CreateUpdateSchedulePaylo func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string aclValues []string isValid bool @@ -136,46 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -207,7 +168,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -233,11 +194,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/os-update/schedule/delete/delete.go b/internal/cmd/server/os-update/schedule/delete/delete.go index 0d2762d65..8fddbb7bd 100644 --- a/internal/cmd/server/os-update/schedule/delete/delete.go +++ b/internal/cmd/server/os-update/schedule/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +15,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -28,7 +29,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", scheduleIdArg), Short: "Deletes a Server os-update Schedule", @@ -52,12 +53,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete server os-update schedule %q? (This cannot be undone)", model.ScheduleId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete server os-update schedule %q? (This cannot be undone)", model.ScheduleId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -96,19 +95,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiDeleteUpdateScheduleRequest { - req := apiClient.DeleteUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) + req := apiClient.DefaultAPI.DeleteUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) return req } diff --git a/internal/cmd/server/os-update/schedule/delete/delete_test.go b/internal/cmd/server/os-update/schedule/delete/delete_test.go index eb005e164..c965330f4 100644 --- a/internal/cmd/server/os-update/schedule/delete/delete_test.go +++ b/internal/cmd/server/os-update/schedule/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -22,7 +21,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -65,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiDeleteUpdateScheduleRequest)) serverupdate.ApiDeleteUpdateScheduleRequest { - request := testClient.DeleteUpdateSchedule(testCtx, testProjectId, testServerId, testUpdateScheduleId, testRegion) + request := testClient.DefaultAPI.DeleteUpdateSchedule(testCtx, testProjectId, testServerId, testUpdateScheduleId, testRegion) for _, mod := range mods { mod(&request) } @@ -133,54 +132,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,7 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/os-update/schedule/describe/describe.go b/internal/cmd/server/os-update/schedule/describe/describe.go index 89aaba7b0..f926701ba 100644 --- a/internal/cmd/server/os-update/schedule/describe/describe.go +++ b/internal/cmd/server/os-update/schedule/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) const ( @@ -31,7 +30,7 @@ type inputModel struct { ScheduleId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", scheduleIdArg), Short: "Shows details of a Server os-update Schedule", @@ -92,52 +91,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ScheduleId: scheduleId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiGetUpdateScheduleRequest { - req := apiClient.GetUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) + req := apiClient.DefaultAPI.GetUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) return req } func outputResult(p *print.Printer, outputFormat string, schedule serverupdate.UpdateSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(schedule, "", " ") - if err != nil { - return fmt.Errorf("marshal server os-update schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(schedule, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server os-update schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, schedule, func() error { table := tables.NewTable() - table.AddRow("SCHEDULE ID", utils.PtrString(schedule.Id)) + table.AddRow("SCHEDULE ID", schedule.Id) table.AddSeparator() - table.AddRow("SCHEDULE NAME", utils.PtrString(schedule.Name)) + table.AddRow("SCHEDULE NAME", schedule.Name) table.AddSeparator() - table.AddRow("ENABLED", utils.PtrString(schedule.Enabled)) + table.AddRow("ENABLED", schedule.Enabled) table.AddSeparator() - table.AddRow("RRULE", utils.PtrString(schedule.Rrule)) + table.AddRow("RRULE", schedule.Rrule) table.AddSeparator() - table.AddRow("MAINTENANCE WINDOW", utils.PtrString(schedule.MaintenanceWindow)) + table.AddRow("MAINTENANCE WINDOW", schedule.MaintenanceWindow) table.AddSeparator() err := table.Display(p) @@ -146,5 +120,5 @@ func outputResult(p *print.Printer, outputFormat string, schedule serverupdate.U } return nil - } + }) } diff --git a/internal/cmd/server/os-update/schedule/describe/describe_test.go b/internal/cmd/server/os-update/schedule/describe/describe_test.go index 178585383..c9fc42390 100644 --- a/internal/cmd/server/os-update/schedule/describe/describe_test.go +++ b/internal/cmd/server/os-update/schedule/describe/describe_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -65,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiGetUpdateScheduleRequest)) serverupdate.ApiGetUpdateScheduleRequest { - request := testClient.GetUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId, testRegion) + request := testClient.DefaultAPI.GetUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId, testRegion) for _, mod := range mods { mod(&request) } @@ -133,54 +133,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -206,7 +159,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -231,11 +184,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.schedule); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.schedule); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/os-update/schedule/list/list.go b/internal/cmd/server/os-update/schedule/list/list.go index 3f8eeb6cb..95f90665a 100644 --- a/internal/cmd/server/os-update/schedule/list/list.go +++ b/internal/cmd/server/os-update/schedule/list/list.go @@ -2,14 +2,15 @@ package list import ( "context" - "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + iaasClient "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +20,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) const ( @@ -34,7 +33,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server os-update schedules", @@ -48,9 +47,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all os-update schedules for a server with ID "xxx" in JSON format`, "$ stackit server os-update schedule list --server-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -67,27 +66,25 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server os-update schedules: %w", err) } - schedules := *resp.Items - if len(schedules) == 0 { - serverLabel := model.ServerId - // Get server name - if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { - serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient, model.ProjectId, model.ServerId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - } else if serverName != "" { - serverLabel = serverName - } + + schedules := resp.GetItems() + + serverLabel := model.ServerId + // Get server name + if iaasApiClient, err := iaasClient.ConfigureClient(params.Printer, params.CliVersion); err == nil { + serverName, err := iaasUtils.GetServerName(ctx, iaasApiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) + } else if serverName != "" { + serverLabel = serverName } - params.Printer.Info("No os-update schedules found for server %s\n", serverLabel) - return nil } // Truncate output if model.Limit != nil && len(schedules) > int(*model.Limit) { schedules = schedules[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, schedules) + return outputResult(params.Printer, model.OutputFormat, serverLabel, schedules) }, } configureFlags(cmd) @@ -102,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -122,52 +119,31 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient) serverupdate.ApiListUpdateSchedulesRequest { - req := apiClient.ListUpdateSchedules(ctx, model.ProjectId, model.ServerId, model.Region) + req := apiClient.DefaultAPI.ListUpdateSchedules(ctx, model.ProjectId, model.ServerId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, schedules []serverupdate.UpdateSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(schedules, "", " ") - if err != nil { - return fmt.Errorf("marshal Server os-update Schedules list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(schedules, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal Server os-update Schedules list: %w", err) +func outputResult(p *print.Printer, outputFormat, serverLabel string, schedules []serverupdate.UpdateSchedule) error { + return p.OutputResult(outputFormat, schedules, func() error { + if len(schedules) == 0 { + p.Outputf("No os-update schedules found for server %s\n", serverLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("SCHEDULE ID", "SCHEDULE NAME", "ENABLED", "RRULE", "MAINTENANCE WINDOW") for i := range schedules { s := schedules[i] table.AddRow( - utils.PtrString(s.Id), - utils.PtrString(s.Name), - utils.PtrString(s.Enabled), - utils.PtrString(s.Rrule), - utils.PtrString(s.MaintenanceWindow), + s.Id, + s.Name, + s.Enabled, + s.Rrule, + s.MaintenanceWindow, ) } err := table.Display(p) @@ -175,5 +151,5 @@ func outputResult(p *print.Printer, outputFormat string, schedules []serverupdat return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/os-update/schedule/list/list_test.go b/internal/cmd/server/os-update/schedule/list/list_test.go index 57aa1c0dc..4534e1718 100644 --- a/internal/cmd/server/os-update/schedule/list/list_test.go +++ b/internal/cmd/server/os-update/schedule/list/list_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( @@ -22,7 +22,7 @@ const ( type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -56,7 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdateSchedulesRequest)) serverupdate.ApiListUpdateSchedulesRequest { - request := testClient.ListUpdateSchedules(testCtx, testProjectId, testServerId, testRegion) + request := testClient.DefaultAPI.ListUpdateSchedules(testCtx, testProjectId, testServerId, testRegion) for _, mod := range mods { mod(&request) } @@ -66,6 +66,7 @@ func fixtureRequest(mods ...func(request *serverupdate.ApiListUpdateSchedulesReq func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -120,46 +121,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -183,7 +145,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -195,6 +157,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + serverLabel string schedules []serverupdate.UpdateSchedule } tests := []struct { @@ -208,11 +171,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.schedules); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.schedules); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/os-update/schedule/schedule.go b/internal/cmd/server/os-update/schedule/schedule.go index 3ffeb36ea..d3ccd6b63 100644 --- a/internal/cmd/server/os-update/schedule/schedule.go +++ b/internal/cmd/server/os-update/schedule/schedule.go @@ -1,19 +1,19 @@ package schedule import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/schedule/create" del "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/schedule/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/schedule/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/schedule/list" "github.com/stackitcloud/stackit-cli/internal/cmd/server/os-update/schedule/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "schedule", Short: "Provides functionality for Server os-update Schedule", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) diff --git a/internal/cmd/server/os-update/schedule/update/update.go b/internal/cmd/server/os-update/schedule/update/update.go index 3394ba5ba..54609394f 100644 --- a/internal/cmd/server/os-update/schedule/update/update.go +++ b/internal/cmd/server/os-update/schedule/update/update.go @@ -2,12 +2,13 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,8 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/serverosupdate/client" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) const ( @@ -41,10 +40,10 @@ type inputModel struct { ScheduleName *string Enabled *bool Rrule *string - MaintenanceWindow *int64 + MaintenanceWindow *int32 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", scheduleIdArg), Short: "Updates a Server os-update Schedule", @@ -69,18 +68,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - currentSchedule, err := apiClient.GetUpdateScheduleExecute(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) + currentSchedule, err := apiClient.DefaultAPI.GetUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region).Execute() if err != nil { params.Printer.Debug(print.ErrorLevel, "get current server os-update schedule: %v", err) return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update Server os-update Schedule %q?", model.ScheduleId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update Server os-update Schedule %q?", model.ScheduleId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -104,7 +101,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") cmd.Flags().StringP(nameFlag, "n", "", "os-update schedule name") - cmd.Flags().Int64P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)") + cmd.Flags().Int32P(maintenanceWindowFlag, "d", defaultMaintenanceWindow, "Maintenance window (in hours, 1-24)") cmd.Flags().BoolP(enabledFlag, "e", defaultEnabled, "Is the server os-update schedule enabled") cmd.Flags().StringP(rruleFlag, "r", defaultRrule, "os-update RRULE (recurrence rule)") @@ -125,37 +122,29 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ScheduleId: scheduleId, ScheduleName: flags.FlagToStringPointer(p, cmd, nameFlag), ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), - MaintenanceWindow: flags.FlagToInt64Pointer(p, cmd, maintenanceWindowFlag), + MaintenanceWindow: flags.FlagToInt32Pointer(p, cmd, maintenanceWindowFlag), Rrule: flags.FlagToStringPointer(p, cmd, rruleFlag), Enabled: flags.FlagToBoolPointer(p, cmd, enabledFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdate.APIClient, old serverupdate.UpdateSchedule) (serverupdate.ApiUpdateUpdateScheduleRequest, error) { - req := apiClient.UpdateUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) + req := apiClient.DefaultAPI.UpdateUpdateSchedule(ctx, model.ProjectId, model.ServerId, model.ScheduleId, model.Region) if model.MaintenanceWindow != nil { - old.MaintenanceWindow = model.MaintenanceWindow + old.MaintenanceWindow = *model.MaintenanceWindow } if model.Enabled != nil { - old.Enabled = model.Enabled + old.Enabled = *model.Enabled } if model.ScheduleName != nil { - old.Name = model.ScheduleName + old.Name = *model.ScheduleName } if model.Rrule != nil { - old.Rrule = model.Rrule + old.Rrule = *model.Rrule } req = req.UpdateUpdateSchedulePayload(serverupdate.UpdateUpdateSchedulePayload{ @@ -168,25 +157,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *serverupdat } func outputResult(p *print.Printer, outputFormat string, resp serverupdate.UpdateSchedule) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal update server os-update schedule: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal update server os-update schedule: %w", err) - } - p.Outputln(string(details)) - + return p.OutputResult(outputFormat, resp, func() error { + p.Info("Updated server os-update schedule %d\n", resp.Id) return nil - default: - p.Info("Updated server os-update schedule %s\n", utils.PtrString(resp.Id)) - return nil - } + }) } diff --git a/internal/cmd/server/os-update/schedule/update/update_test.go b/internal/cmd/server/os-update/schedule/update/update_test.go index a19c77958..edda8debe 100644 --- a/internal/cmd/server/os-update/schedule/update/update_test.go +++ b/internal/cmd/server/os-update/schedule/update/update_test.go @@ -2,35 +2,33 @@ package update import ( "context" - "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) const ( testRegion = "eu02" - testScheduleId = "5" + testScheduleId = int32(5) ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serverupdate.APIClient{} +var testClient = &serverupdate.APIClient{DefaultAPI: &serverupdate.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ - testScheduleId, + string(testScheduleId), } for _, mod := range mods { mod(argValues) @@ -61,12 +59,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - ScheduleId: testScheduleId, + ScheduleId: string(testScheduleId), ServerId: testServerId, ScheduleName: utils.Ptr("example-schedule-name"), Enabled: utils.Ptr(defaultEnabled), Rrule: utils.Ptr(defaultRrule), - MaintenanceWindow: utils.Ptr(int64(23)), + MaintenanceWindow: utils.Ptr(int32(23)), } for _, mod := range mods { mod(model) @@ -75,13 +73,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureUpdateSchedule(mods ...func(schedule *serverupdate.UpdateSchedule)) *serverupdate.UpdateSchedule { - id, _ := strconv.ParseInt(testScheduleId, 10, 64) schedule := &serverupdate.UpdateSchedule{ - Name: utils.Ptr("example-schedule-name"), - Id: utils.Ptr(id), - Enabled: utils.Ptr(defaultEnabled), - Rrule: utils.Ptr(defaultRrule), - MaintenanceWindow: utils.Ptr(int64(23)), + Name: "example-schedule-name", + Id: testScheduleId, + Enabled: defaultEnabled, + Rrule: defaultRrule, + MaintenanceWindow: int32(23), } for _, mod := range mods { mod(schedule) @@ -91,10 +88,10 @@ func fixtureUpdateSchedule(mods ...func(schedule *serverupdate.UpdateSchedule)) func fixturePayload(mods ...func(payload *serverupdate.UpdateUpdateSchedulePayload)) serverupdate.UpdateUpdateSchedulePayload { payload := serverupdate.UpdateUpdateSchedulePayload{ - Name: utils.Ptr("example-schedule-name"), - Enabled: utils.Ptr(defaultEnabled), - Rrule: utils.Ptr("DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1"), - MaintenanceWindow: utils.Ptr(int64(23)), + Name: "example-schedule-name", + Enabled: defaultEnabled, + Rrule: "DTSTART;TZID=Europe/Sofia:20200803T023000 RRULE:FREQ=DAILY;INTERVAL=1", + MaintenanceWindow: int32(23), } for _, mod := range mods { mod(&payload) @@ -103,7 +100,7 @@ func fixturePayload(mods ...func(payload *serverupdate.UpdateUpdateSchedulePaylo } func fixtureRequest(mods ...func(request *serverupdate.ApiUpdateUpdateScheduleRequest)) serverupdate.ApiUpdateUpdateScheduleRequest { - request := testClient.UpdateUpdateSchedule(testCtx, testProjectId, testServerId, testScheduleId, testRegion) + request := testClient.DefaultAPI.UpdateUpdateSchedule(testCtx, testProjectId, testServerId, string(testScheduleId), testRegion) request = request.UpdateUpdateSchedulePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -173,8 +170,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -214,7 +211,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flag groups: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -260,7 +257,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serverupdate.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -285,11 +282,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.resp); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/public-ip/attach/attach.go b/internal/cmd/server/public-ip/attach/attach.go index 843c030e7..28307f9ea 100644 --- a/internal/cmd/server/public-ip/attach/attach.go +++ b/internal/cmd/server/public-ip/attach/attach.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -26,11 +28,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string PublicIpId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("attach %s", publicIpIdArg), Short: "Attaches a public IP to a server", @@ -54,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.PublicIpId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get public ip name: %v", err) publicIpLabel = model.PublicIpId @@ -62,20 +64,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { publicIpLabel = model.PublicIpId } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to attach public IP %q to server %q?", publicIpLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to attach public IP %q to server %q?", publicIpLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -109,22 +109,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), PublicIpId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddPublicIpToServerRequest { - return apiClient.AddPublicIpToServer(ctx, model.ProjectId, *model.ServerId, model.PublicIpId) + return apiClient.DefaultAPI.AddPublicIpToServer(ctx, model.ProjectId, model.Region, model.ServerId, model.PublicIpId) } diff --git a/internal/cmd/server/public-ip/attach/attach_test.go b/internal/cmd/server/public-ip/attach/attach_test.go index c58f7b280..fac9945d4 100644 --- a/internal/cmd/server/public-ip/attach/attach_test.go +++ b/internal/cmd/server/public-ip/attach/attach_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testPublicIpId = uuid.NewString() @@ -37,8 +38,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -51,8 +54,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, PublicIpId: testPublicIpId, } for _, mod := range mods { @@ -62,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiAddPublicIpToServerRequest)) iaas.ApiAddPublicIpToServerRequest { - request := testClient.AddPublicIpToServer(testCtx, testProjectId, testServerId, testPublicIpId) + request := testClient.DefaultAPI.AddPublicIpToServer(testCtx, testProjectId, testRegion, testServerId, testPublicIpId) for _, mod := range mods { mod(&request) } @@ -94,7 +98,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -102,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -110,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -147,8 +151,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -180,7 +184,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -218,7 +222,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/public-ip/detach/detach.go b/internal/cmd/server/public-ip/detach/detach.go index 3c1aa6687..7dabbf5f8 100644 --- a/internal/cmd/server/public-ip/detach/detach.go +++ b/internal/cmd/server/public-ip/detach/detach.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -26,11 +28,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string PublicIpId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("detach %s", publicIpIdArg), Short: "Detaches a public IP from a server", @@ -55,7 +57,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient, model.ProjectId, model.PublicIpId) + publicIpLabel, _, err := iaasUtils.GetPublicIP(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.PublicIpId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get public ip: %v", err) publicIpLabel = model.PublicIpId @@ -63,20 +65,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { publicIpLabel = model.PublicIpId } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to detach public IP %q from server %q?", publicIpLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to detach public IP %q from server %q?", publicIpLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -110,22 +110,14 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), PublicIpId: publicIpId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemovePublicIpFromServerRequest { - return apiClient.RemovePublicIpFromServer(ctx, model.ProjectId, *model.ServerId, model.PublicIpId) + return apiClient.DefaultAPI.RemovePublicIpFromServer(ctx, model.ProjectId, model.Region, model.ServerId, model.PublicIpId) } diff --git a/internal/cmd/server/public-ip/detach/detach_test.go b/internal/cmd/server/public-ip/detach/detach_test.go index 8a46591a7..4260b4881 100644 --- a/internal/cmd/server/public-ip/detach/detach_test.go +++ b/internal/cmd/server/public-ip/detach/detach_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testPublicIpId = uuid.NewString() @@ -37,8 +38,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -51,8 +53,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, PublicIpId: testPublicIpId, } for _, mod := range mods { @@ -62,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiRemovePublicIpFromServerRequest)) iaas.ApiRemovePublicIpFromServerRequest { - request := testClient.RemovePublicIpFromServer(testCtx, testProjectId, testServerId, testPublicIpId) + request := testClient.DefaultAPI.RemovePublicIpFromServer(testCtx, testProjectId, testRegion, testServerId, testPublicIpId) for _, mod := range mods { mod(&request) } @@ -93,7 +96,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -101,7 +104,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -109,7 +112,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -146,8 +149,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -179,7 +182,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -217,7 +220,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/public-ip/public_ip.go b/internal/cmd/server/public-ip/public_ip.go index 494993ce8..db04a0a67 100644 --- a/internal/cmd/server/public-ip/public_ip.go +++ b/internal/cmd/server/public-ip/public_ip.go @@ -1,16 +1,16 @@ package publicip import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/public-ip/attach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/public-ip/detach" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "public-ip", Short: "Allows attaching/detaching public IPs to servers", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(attach.NewCmd(params)) cmd.AddCommand(detach.NewCmd(params)) } diff --git a/internal/cmd/server/reboot/reboot.go b/internal/cmd/server/reboot/reboot.go index dbdc10ef1..5b81f4bc6 100644 --- a/internal/cmd/server/reboot/reboot.go +++ b/internal/cmd/server/reboot/reboot.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -33,7 +35,7 @@ type inputModel struct { HardReboot bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("reboot %s", serverIdArg), Short: "Reboots a server", @@ -62,19 +64,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId } else if serverLabel == "" { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to reboot server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to reboot server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,20 +111,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu HardReboot: flags.FlagToBoolValue(p, cmd, hardRebootFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRebootServerRequest { - req := apiClient.RebootServer(ctx, model.ProjectId, model.ServerId) + req := apiClient.DefaultAPI.RebootServer(ctx, model.ProjectId, model.Region, model.ServerId) // if hard reboot is set the action must be set (soft is default) if model.HardReboot { req = req.Action(hardRebootAction) diff --git a/internal/cmd/server/reboot/reboot_test.go b/internal/cmd/server/reboot/reboot_test.go index cfcda6783..a6664291b 100644 --- a/internal/cmd/server/reboot/reboot_test.go +++ b/internal/cmd/server/reboot/reboot_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -35,7 +36,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + hardRebootFlag: "false", } for _, mod := range mods { @@ -49,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, HardReboot: false, @@ -60,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiRebootServerRequest)) iaas.ApiRebootServerRequest { - request := testClient.RebootServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.RebootServer(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -98,7 +102,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -106,7 +110,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -130,54 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +156,7 @@ func TestBuildRequest(t *testing.T) { model.HardReboot = true }), expectedRequest: fixtureRequest(func(request *iaas.ApiRebootServerRequest) { - *request = (*request).Action("hard") + *request = request.Action("hard") }), }, } @@ -210,7 +167,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/rescue/rescue.go b/internal/cmd/server/rescue/rescue.go index 5c4efea4f..363b12a3e 100644 --- a/internal/cmd/server/rescue/rescue.go +++ b/internal/cmd/server/rescue/rescue.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,8 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -30,10 +32,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel ServerId string - ImageId *string + ImageId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("rescue %s", serverIdArg), Short: "Rescues an existing server", @@ -58,7 +60,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -66,12 +68,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to rescue server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to rescue server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -83,20 +83,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Rescuing server") - _, err = wait.RescueServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Rescuing server", func() error { + _, err = wait.RescueServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server rescuing: %w", err) } - s.Stop() } operationState := "Rescued" if model.Async { operationState = "Triggered rescue of" } - params.Printer.Info("%s server %q. Image %q is used as temporary boot image\n", operationState, serverLabel, utils.PtrString(model.ImageId)) + params.Printer.Info("%s server %q. Image %q is used as temporary boot image\n", operationState, serverLabel, model.ImageId) return nil }, @@ -123,23 +123,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, ServerId: serverId, - ImageId: flags.FlagToStringPointer(p, cmd, imageIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + ImageId: flags.FlagToStringValue(p, cmd, imageIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRescueServerRequest { - req := apiClient.RescueServer(ctx, model.ProjectId, model.ServerId) + req := apiClient.DefaultAPI.RescueServer(ctx, model.ProjectId, model.Region, model.ServerId) payload := iaas.RescueServerPayload{ Image: model.ImageId, } diff --git a/internal/cmd/server/rescue/rescue_test.go b/internal/cmd/server/rescue/rescue_test.go index 8a5695fd9..9a66072f5 100644 --- a/internal/cmd/server/rescue/rescue_test.go +++ b/internal/cmd/server/rescue/rescue_test.go @@ -4,23 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testImageId = uuid.NewString() @@ -37,8 +37,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - imageIdFlag: testImageId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + imageIdFlag: testImageId, } for _, mod := range mods { mod(flagValues) @@ -51,9 +53,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, - ImageId: utils.Ptr(testImageId), + ImageId: testImageId, } for _, mod := range mods { mod(model) @@ -62,9 +65,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiRescueServerRequest)) iaas.ApiRescueServerRequest { - request := testClient.RescueServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.RescueServer(testCtx, testProjectId, testRegion, testServerId) request = request.RescueServerPayload(iaas.RescueServerPayload{ - Image: utils.Ptr(testImageId), + Image: testImageId, }) for _, mod := range mods { mod(&request) @@ -105,7 +108,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +116,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +124,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -145,54 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -216,7 +172,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/resize/resize.go b/internal/cmd/server/resize/resize.go index 57e6ebe1b..b6a7840f3 100644 --- a/internal/cmd/server/resize/resize.go +++ b/internal/cmd/server/resize/resize.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,8 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -30,10 +32,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel ServerId string - MachineType *string + MachineType string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("resize %s", serverIdArg), Short: "Resizes the server to the given machine type", @@ -58,7 +60,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -66,12 +68,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to resize server %q to machine type %q?", serverLabel, *model.MachineType) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to resize server %q to machine type %q?", serverLabel, model.MachineType) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -83,13 +83,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Resizing server") - _, err = wait.ResizeServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Resizing server", func() error { + _, err = wait.ResizeServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server resizing: %w", err) } - s.Stop() } operationState := "Resized" @@ -106,7 +106,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } func configureFlags(cmd *cobra.Command) { - cmd.Flags().String(machineTypeFlag, "", "Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html") + cmd.Flags().String(machineTypeFlag, "", "Name of the type of the machine for the server. Possible values are documented in https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/") err := flags.MarkFlagsRequired(cmd, machineTypeFlag) cobra.CheckErr(err) @@ -123,23 +123,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, ServerId: serverId, - MachineType: flags.FlagToStringPointer(p, cmd, machineTypeFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + MachineType: flags.FlagToStringValue(p, cmd, machineTypeFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiResizeServerRequest { - req := apiClient.ResizeServer(ctx, model.ProjectId, model.ServerId) + req := apiClient.DefaultAPI.ResizeServer(ctx, model.ProjectId, model.Region, model.ServerId) payload := iaas.ResizeServerPayload{ MachineType: model.MachineType, } diff --git a/internal/cmd/server/resize/resize_test.go b/internal/cmd/server/resize/resize_test.go index ee94cfe6e..3739ab759 100644 --- a/internal/cmd/server/resize/resize_test.go +++ b/internal/cmd/server/resize/resize_test.go @@ -4,23 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -36,7 +36,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + machineTypeFlag: "t1.2", } for _, mod := range mods { @@ -50,9 +52,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, - MachineType: utils.Ptr("t1.2"), + MachineType: "t1.2", } for _, mod := range mods { mod(model) @@ -61,9 +64,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiResizeServerRequest)) iaas.ApiResizeServerRequest { - request := testClient.ResizeServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.ResizeServer(testCtx, testProjectId, testRegion, testServerId) request = request.ResizeServerPayload(iaas.ResizeServerPayload{ - MachineType: utils.Ptr("t1.2"), + MachineType: "t1.2", }) for _, mod := range mods { mod(&request) @@ -104,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -112,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -120,7 +123,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -144,54 +147,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -215,7 +171,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/security-group/attach/attach.go b/internal/cmd/server/security-group/attach/attach.go new file mode 100644 index 000000000..1d2d54c9f --- /dev/null +++ b/internal/cmd/server/security-group/attach/attach.go @@ -0,0 +1,120 @@ +package attach + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" +) + +const ( + serverIdFlag = "server-id" + securityGroupIdFlag = "security-group-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string + SecurityGroupId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "attach", + Short: "Attaches a security group to a server", + Long: "Attaches a security group to a server.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Attach a security group with ID "xxx" to a server with ID "yyy"`, + `$ stackit server security-group attach --server-id yyy --security-group-id xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) + serverLabel = model.ServerId + } else if serverLabel == "" { + serverLabel = model.ServerId + } + + securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) + securityGroupLabel = model.SecurityGroupId + } + + prompt := fmt.Sprintf("Are you sure you want to attach security group %q to server %q?", securityGroupLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + if err := req.Execute(); err != nil { + return fmt.Errorf("attach security group to server: %w", err) + } + + params.Printer.Outputf("Attached security group %q to server %q\n", securityGroupLabel, serverLabel) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), serverIdFlag, "Server ID") + cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, "Security Group ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag, securityGroupIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddSecurityGroupToServerRequest { + req := apiClient.DefaultAPI.AddSecurityGroupToServer(ctx, model.ProjectId, model.Region, model.ServerId, model.SecurityGroupId) + return req +} diff --git a/internal/cmd/server/security-group/attach/attach_test.go b/internal/cmd/server/security-group/attach/attach_test.go new file mode 100644 index 000000000..30465b515 --- /dev/null +++ b/internal/cmd/server/security-group/attach/attach_test.go @@ -0,0 +1,182 @@ +package attach + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() +var testSecurityGroupId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, + securityGroupIdFlag: testSecurityGroupId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + ServerId: testServerId, + SecurityGroupId: testSecurityGroupId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiAddSecurityGroupToServerRequest)) iaas.ApiAddSecurityGroupToServerRequest { + request := testClient.DefaultAPI.AddSecurityGroupToServer(testCtx, testProjectId, testRegion, testServerId, testSecurityGroupId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "security group id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, securityGroupIdFlag) + }), + isValid: false, + }, + { + description: "security group id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[securityGroupIdFlag] = "" + }), + isValid: false, + }, + { + description: "security group id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[securityGroupIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "server id flag missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, serverIdFlag) + }), + isValid: false, + }, + { + description: "server id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "" + }), + isValid: false, + }, + { + description: "server id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, []string{}, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiAddSecurityGroupToServerRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/server/security-group/detach/detach.go b/internal/cmd/server/security-group/detach/detach.go new file mode 100644 index 000000000..54ccf89f0 --- /dev/null +++ b/internal/cmd/server/security-group/detach/detach.go @@ -0,0 +1,120 @@ +package detach + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" + iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" +) + +const ( + serverIdFlag = "server-id" + securityGroupIdFlag = "security-group-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + ServerId string + SecurityGroupId string +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "detach", + Short: "Detaches a security group from a server", + Long: "Detaches a security group from a server.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Detach a security group with ID "xxx" from a server with ID "yyy"`, + `$ stackit server security-group detach --server-id yyy --security-group-id xxx`, + ), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) + serverLabel = model.ServerId + } else if serverLabel == "" { + serverLabel = model.ServerId + } + + securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SecurityGroupId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) + securityGroupLabel = model.SecurityGroupId + } + + prompt := fmt.Sprintf("Are you sure you want to detach security group %q from server %q?", securityGroupLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + if err := req.Execute(); err != nil { + return fmt.Errorf("detach security group from server: %w", err) + } + + params.Printer.Outputf("Detached security group %q from server %q\n", securityGroupLabel, serverLabel) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), serverIdFlag, "Server ID") + cmd.Flags().Var(flags.UUIDFlag(), securityGroupIdFlag, "Security Group ID") + + err := flags.MarkFlagsRequired(cmd, serverIdFlag, securityGroupIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), + SecurityGroupId: flags.FlagToStringValue(p, cmd, securityGroupIdFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveSecurityGroupFromServerRequest { + req := apiClient.DefaultAPI.RemoveSecurityGroupFromServer(ctx, model.ProjectId, model.Region, model.ServerId, model.SecurityGroupId) + return req +} diff --git a/internal/cmd/server/security-group/detach/detach_test.go b/internal/cmd/server/security-group/detach/detach_test.go new file mode 100644 index 000000000..7cc01f278 --- /dev/null +++ b/internal/cmd/server/security-group/detach/detach_test.go @@ -0,0 +1,182 @@ +package detach + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +const ( + testRegion = "eu01" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} +var testProjectId = uuid.NewString() +var testServerId = uuid.NewString() +var testSecurityGroupId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, + securityGroupIdFlag: testSecurityGroupId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + ProjectId: testProjectId, + Region: testRegion, + }, + ServerId: testServerId, + SecurityGroupId: testSecurityGroupId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *iaas.ApiRemoveSecurityGroupFromServerRequest)) iaas.ApiRemoveSecurityGroupFromServerRequest { + request := testClient.DefaultAPI.RemoveSecurityGroupFromServer(testCtx, testProjectId, testRegion, testServerId, testSecurityGroupId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, globalflags.ProjectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "security group id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, securityGroupIdFlag) + }), + isValid: false, + }, + { + description: "security group id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[securityGroupIdFlag] = "" + }), + isValid: false, + }, + { + description: "security group id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[securityGroupIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "server id flag missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, serverIdFlag) + }), + isValid: false, + }, + { + description: "server id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "" + }), + isValid: false, + }, + { + description: "server id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[serverIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, []string{}, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest iaas.ApiRemoveSecurityGroupFromServerRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/server/security-group/security-group.go b/internal/cmd/server/security-group/security-group.go new file mode 100644 index 000000000..aed9bfa4a --- /dev/null +++ b/internal/cmd/server/security-group/security-group.go @@ -0,0 +1,28 @@ +package securitygroup + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/server/security-group/attach" + "github.com/stackitcloud/stackit-cli/internal/cmd/server/security-group/detach" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "security-group", + Short: "Allows attaching/detaching security groups to servers", + Long: "Allows attaching/detaching security groups to servers.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, params) + return cmd +} + +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(attach.NewCmd(params)) + cmd.AddCommand(detach.NewCmd(params)) +} diff --git a/internal/cmd/server/server.go b/internal/cmd/server/server.go index ac3101b41..3cb8a5ffc 100644 --- a/internal/cmd/server/server.go +++ b/internal/cmd/server/server.go @@ -1,7 +1,6 @@ package server import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/backup" "github.com/stackitcloud/stackit-cli/internal/cmd/server/command" "github.com/stackitcloud/stackit-cli/internal/cmd/server/console" @@ -18,12 +17,14 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/server/reboot" "github.com/stackitcloud/stackit-cli/internal/cmd/server/rescue" "github.com/stackitcloud/stackit-cli/internal/cmd/server/resize" + securitygroup "github.com/stackitcloud/stackit-cli/internal/cmd/server/security-group" serviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/server/service-account" "github.com/stackitcloud/stackit-cli/internal/cmd/server/start" "github.com/stackitcloud/stackit-cli/internal/cmd/server/stop" "github.com/stackitcloud/stackit-cli/internal/cmd/server/unrescue" "github.com/stackitcloud/stackit-cli/internal/cmd/server/update" "github.com/stackitcloud/stackit-cli/internal/cmd/server/volume" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -31,7 +32,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "server", Short: "Provides functionality for servers", @@ -43,7 +44,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(backup.NewCmd(params)) cmd.AddCommand(command.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) @@ -51,6 +52,7 @@ func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(publicip.NewCmd(params)) + cmd.AddCommand(securitygroup.NewCmd(params)) cmd.AddCommand(serviceaccount.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) cmd.AddCommand(volume.NewCmd(params)) diff --git a/internal/cmd/server/service-account/attach/attach.go b/internal/cmd/server/service-account/attach/attach.go index 89232fa88..b32ff05ee 100644 --- a/internal/cmd/server/service-account/attach/attach.go +++ b/internal/cmd/server/service-account/attach/attach.go @@ -2,10 +2,10 @@ package attach import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,33 +15,34 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( - serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" + serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed after 2026-12-31 serverIdFlag = "server-id" + + serviceAccFlag = "service-account-email" ) type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string ServiceAccMail string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: fmt.Sprintf("attach %s", serviceAccMailArg), + Use: "attach", Short: "Attach a service account to a server", Long: "Attach a service account to a server", - Args: args.SingleArg(serviceAccMailArg, nil), + Args: args.SingleOptionalArg(serviceAccMailArg, nil), Example: examples.Build( examples.NewExample( `Attach a service account with mail "xxx@sa.stackit.cloud" to a server with ID "yyy"`, - "$ stackit server service-account attach xxx@sa.stackit.cloud --server-id yyy", + "$ stackit server service-account attach --service-account-email xxx@sa.stackit.cloud --server-id yyy", ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -56,20 +57,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to attach service account %q to server %q?", model.ServiceAccMail, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to attach service account %q to server %q?", model.ServiceAccMail, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -88,61 +87,45 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server ID") - + cmd.Flags().VarP(flags.EmailFlag(), serviceAccFlag, "a", "Service Account Email") err := flags.MarkFlagsRequired(cmd, serverIdFlag) cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - serviceAccMail := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + var serviceAccMail string + if cmd.Flags().Changed(serviceAccFlag) { + serviceAccMail = flags.FlagToStringValue(p, cmd, serviceAccFlag) + } else if len(inputArgs) > 0 { + serviceAccMail = inputArgs[0] + p.Warn("Using a positional argument for the service account email is deprecated and will be removed after 2026-12. Please use the '--%s' flag instead.\n", serviceAccFlag) + } else { + return nil, fmt.Errorf(`service account must be specified by using either the --%s flag or (deprecated) as a positional argument`, serviceAccFlag) + } + model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), ServiceAccMail: serviceAccMail, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddServiceAccountToServerRequest { - req := apiClient.AddServiceAccountToServer(ctx, model.ProjectId, *model.ServerId, model.ServiceAccMail) + req := apiClient.DefaultAPI.AddServiceAccountToServer(ctx, model.ProjectId, model.Region, model.ServerId, model.ServiceAccMail) return req } func outputResult(p *print.Printer, outputFormat, serviceAccMail, serverLabel string, serviceAccounts iaas.ServiceAccountMailListResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(serviceAccounts, "", " ") - if err != nil { - return fmt.Errorf("marshal service account: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(serviceAccounts, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal service account: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, serviceAccounts, func() error { p.Outputf("Attached service account %q to server %q\n", serviceAccMail, serverLabel) return nil - } + }) } diff --git a/internal/cmd/server/service-account/attach/attach_test.go b/internal/cmd/server/service-account/attach/attach_test.go index 96ed151ce..b7affe1ce 100644 --- a/internal/cmd/server/service-account/attach/attach_test.go +++ b/internal/cmd/server/service-account/attach/attach_test.go @@ -4,23 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testServiceAccount = "test@example.com" @@ -37,8 +39,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -51,8 +55,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, ServiceAccMail: testServiceAccount, } for _, mod := range mods { @@ -62,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiAddServiceAccountToServerRequest)) iaas.ApiAddServiceAccountToServerRequest { - request := testClient.AddServiceAccountToServer(testCtx, testProjectId, testServerId, testServiceAccount) + request := testClient.DefaultAPI.AddServiceAccountToServer(testCtx, testProjectId, testRegion, testServerId, testServiceAccount) for _, mod := range mods { mod(&request) } @@ -94,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -102,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -110,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -146,8 +151,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -179,7 +184,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -217,7 +222,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -244,11 +249,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serviceAccMail, tt.args.serverLabel, tt.args.serviceAccounts); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serviceAccMail, tt.args.serverLabel, tt.args.serviceAccounts); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/service-account/detach/detach.go b/internal/cmd/server/service-account/detach/detach.go index 69bc5e4fe..fcbb5f489 100644 --- a/internal/cmd/server/service-account/detach/detach.go +++ b/internal/cmd/server/service-account/detach/detach.go @@ -2,10 +2,10 @@ package detach import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,33 +15,34 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( - serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" + serviceAccMailArg = "SERVICE_ACCOUNT_EMAIL" // Deprecated: positional argument is not used anymore, use the flag instead, will be removed after 2026-12-31 serverIdFlag = "server-id" + + serviceAccFlag = "service-account-email" ) type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string ServiceAccMail string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ - Use: fmt.Sprintf("detach %s", serviceAccMailArg), + Use: "detach", Short: "Detach a service account from a server", Long: "Detach a service account from a server", - Args: args.SingleArg(serviceAccMailArg, nil), + Args: args.SingleOptionalArg(serviceAccMailArg, nil), Example: examples.Build( examples.NewExample( `Detach a service account with mail "xxx@sa.stackit.cloud" from a server "yyy"`, - "$ stackit server service-account detach xxx@sa.stackit.cloud --server-id yyy", + "$ stackit server service-account detach --service-account-email xxx@sa.stackit.cloud --server-id yyy", ), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -56,20 +57,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are your sure you want to detach service account %q from a server %q?", model.ServiceAccMail, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to detach service account %q from a server %q?", model.ServiceAccMail, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -88,61 +87,45 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().VarP(flags.UUIDFlag(), serverIdFlag, "s", "Server id") - + cmd.Flags().VarP(flags.EmailFlag(), serviceAccFlag, "a", "Service Account Email") err := flags.MarkFlagsRequired(cmd, serverIdFlag) cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { - serviceAccMail := inputArgs[0] globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} } + var serviceAccMail string + if cmd.Flags().Changed(serviceAccFlag) { + serviceAccMail = flags.FlagToStringValue(p, cmd, serviceAccFlag) + } else if len(inputArgs) > 0 { + serviceAccMail = inputArgs[0] + p.Warn("Using a positional argument for the service account email is deprecated and will be removed after 2026-12-31. Please use the '--%s' flag instead.\n", serviceAccFlag) + } else { + return nil, fmt.Errorf(`service account must be specified by using either the --%s flag or (deprecated) as a positional argument`, serviceAccFlag) + } + model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), ServiceAccMail: serviceAccMail, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveServiceAccountFromServerRequest { - req := apiClient.RemoveServiceAccountFromServer(ctx, model.ProjectId, *model.ServerId, model.ServiceAccMail) + req := apiClient.DefaultAPI.RemoveServiceAccountFromServer(ctx, model.ProjectId, model.Region, model.ServerId, model.ServiceAccMail) return req } func outputResult(p *print.Printer, outputFormat, serviceAccMail, serverLabel string, service iaas.ServiceAccountMailListResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(service, "", " ") - if err != nil { - return fmt.Errorf("marshal service account: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(service, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal service account: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, service, func() error { p.Outputf("Detached service account %q from server %q\n", serviceAccMail, serverLabel) return nil - } + }) } diff --git a/internal/cmd/server/service-account/detach/detach_test.go b/internal/cmd/server/service-account/detach/detach_test.go index acfd02802..39750bd8b 100644 --- a/internal/cmd/server/service-account/detach/detach_test.go +++ b/internal/cmd/server/service-account/detach/detach_test.go @@ -4,23 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testServiceAccount = "test@example.com" @@ -37,8 +39,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -51,8 +55,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, ServiceAccMail: testServiceAccount, } for _, mod := range mods { @@ -62,7 +67,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiRemoveServiceAccountFromServerRequest)) iaas.ApiRemoveServiceAccountFromServerRequest { - request := testClient.RemoveServiceAccountFromServer(testCtx, testProjectId, testServerId, testServiceAccount) + request := testClient.DefaultAPI.RemoveServiceAccountFromServer(testCtx, testProjectId, testRegion, testServerId, testServiceAccount) for _, mod := range mods { mod(&request) } @@ -94,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -102,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -110,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -146,8 +151,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -179,7 +184,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -217,7 +222,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -244,11 +249,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serviceAccMail, tt.args.serverLabel, tt.args.service); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serviceAccMail, tt.args.serverLabel, tt.args.service); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/service-account/list/list.go b/internal/cmd/server/service-account/list/list.go index 19bfc8fa0..1967b1ad3 100644 --- a/internal/cmd/server/service-account/list/list.go +++ b/internal/cmd/server/service-account/list/list.go @@ -2,10 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -15,9 +15,8 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" - "github.com/goccy/go-yaml" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -28,10 +27,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel Limit *int64 - ServerId *string + ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List all attached service accounts for a server", @@ -51,9 +50,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit server service-account list --server-id xxx --output-format json", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,12 +63,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverName, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverName, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverName = *model.ServerId + serverName = model.ServerId } else if serverName == "" { - serverName = *model.ServerId + serverName = model.ServerId } // Call API @@ -78,17 +77,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list service accounts: %w", err) } - serviceAccounts := *resp.Items - if len(serviceAccounts) == 0 { - params.Printer.Info("No service accounts found for server %s\n", serverName) - return nil - } + serviceAccounts := resp.Items if model.Limit != nil && len(serviceAccounts) > int(*model.Limit) { serviceAccounts = serviceAccounts[:int(*model.Limit)] } - return outputResult(params.Printer, model.OutputFormat, *model.ServerId, serverName, serviceAccounts) + return outputResult(params.Printer, model.OutputFormat, model.ServerId, serverName, serviceAccounts) }, } configureFlags(cmd) @@ -103,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -120,45 +115,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListServerServiceAccountsRequest { - req := apiClient.ListServerServiceAccounts(ctx, model.ProjectId, *model.ServerId) + req := apiClient.DefaultAPI.ListServerServiceAccounts(ctx, model.ProjectId, model.Region, model.ServerId) return req } func outputResult(p *print.Printer, outputFormat, serverId, serverName string, serviceAccounts []string) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(serviceAccounts, "", " ") - if err != nil { - return fmt.Errorf("marshal service accounts list: %w", err) + return p.OutputResult(outputFormat, serviceAccounts, func() error { + if len(serviceAccounts) == 0 { + p.Info("No service accounts found for server %s\n", serverName) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(serviceAccounts, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal service accounts list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("SERVER ID", "SERVER NAME", "SERVICE ACCOUNT") for i := range serviceAccounts { @@ -166,8 +140,8 @@ func outputResult(p *print.Printer, outputFormat, serverId, serverName string, s } err := table.Display(p) if err != nil { - return fmt.Errorf("rednder table: %w", err) + return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/service-account/list/list_test.go b/internal/cmd/server/service-account/list/list_test.go index 9ba1d281a..0b7c21376 100644 --- a/internal/cmd/server/service-account/list/list_test.go +++ b/internal/cmd/server/service-account/list/list_test.go @@ -5,32 +5,36 @@ import ( "strconv" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), &testCtxKey{}, "test") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testLimit = int64(10) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, - limitFlag: strconv.FormatInt(testLimit, 10), + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, + limitFlag: strconv.FormatInt(testLimit, 10), } for _, mod := range mods { mod(flagValues) @@ -43,8 +47,9 @@ func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, Limit: utils.Ptr(testLimit), } for _, mod := range mods { @@ -54,7 +59,7 @@ func fixtureInputModel(mods ...func(inputModel *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListServerServiceAccountsRequest)) iaas.ApiListServerServiceAccountsRequest { - request := testClient.ListServerServiceAccounts(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.ListServerServiceAccounts(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -64,6 +69,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListServerServiceAccountsReque func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -82,21 +88,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -149,46 +155,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -212,7 +179,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Request does not match: %s", diff) @@ -248,11 +215,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverId, tt.args.serverName, tt.args.serviceAccounts); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverId, tt.args.serverName, tt.args.serviceAccounts); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/service-account/service-account.go b/internal/cmd/server/service-account/service-account.go index b9455c563..1f61348a4 100644 --- a/internal/cmd/server/service-account/service-account.go +++ b/internal/cmd/server/service-account/service-account.go @@ -3,7 +3,8 @@ package serviceaccount import ( "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/cmd/server/service-account/attach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/service-account/detach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/service-account/list" @@ -11,7 +12,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "service-account", Short: "Allows attaching/detaching service accounts to servers", @@ -23,7 +24,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(attach.NewCmd(params)) cmd.AddCommand(detach.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) diff --git a/internal/cmd/server/start/start.go b/internal/cmd/server/start/start.go index 5a6f9d02a..907118ee2 100644 --- a/internal/cmd/server/start/start.go +++ b/internal/cmd/server/start/start.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("start %s", serverIdArg), Short: "Starts an existing server or allocates the server if deallocated", @@ -54,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -71,13 +73,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Starting server") - _, err = wait.StartServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Starting server", func() error { + _, err = wait.StartServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server starting: %w", err) } - s.Stop() } operationState := "Started" @@ -105,18 +107,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiStartServerRequest { - return apiClient.StartServer(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.StartServer(ctx, model.ProjectId, model.Region, model.ServerId) } diff --git a/internal/cmd/server/start/start_test.go b/internal/cmd/server/start/start_test.go index 2776c77e7..fffb02378 100644 --- a/internal/cmd/server/start/start_test.go +++ b/internal/cmd/server/start/start_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiStartServerRequest)) iaas.ApiStartServerRequest { - request := testClient.StartServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.StartServer(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -96,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -104,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -128,54 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/stop/stop.go b/internal/cmd/server/stop/stop.go index aa510ef01..5f42f6b34 100644 --- a/internal/cmd/server/stop/stop.go +++ b/internal/cmd/server/stop/stop.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("stop %s", serverIdArg), Short: "Stops an existing server", @@ -54,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -62,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to stop server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to stop server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -79,13 +79,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Stopping server") - _, err = wait.StopServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Stopping server", func() error { + _, err = wait.StopServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server stopping: %w", err) } - s.Stop() } operationState := "Stopped" @@ -113,18 +113,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiStopServerRequest { - return apiClient.StopServer(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.StopServer(ctx, model.ProjectId, model.Region, model.ServerId) } diff --git a/internal/cmd/server/stop/stop_test.go b/internal/cmd/server/stop/stop_test.go index bbaefddba..8ba4144c7 100644 --- a/internal/cmd/server/stop/stop_test.go +++ b/internal/cmd/server/stop/stop_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiStopServerRequest)) iaas.ApiStopServerRequest { - request := testClient.StopServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.StopServer(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -96,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -104,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -128,54 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/unrescue/unrescue.go b/internal/cmd/server/unrescue/unrescue.go index 44bf3ace1..4815baad6 100644 --- a/internal/cmd/server/unrescue/unrescue.go +++ b/internal/cmd/server/unrescue/unrescue.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ type inputModel struct { ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("unrescue %s", serverIdArg), Short: "Unrescues an existing server", @@ -54,7 +56,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -62,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to unrescue server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to unrescue server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -79,13 +79,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Unrescuing server") - _, err = wait.UnrescueServerWaitHandler(ctx, apiClient, model.ProjectId, model.ServerId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Unrescuing server", func() error { + _, err = wait.UnrescueServerWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for server unrescuing: %w", err) } - s.Stop() } operationState := "Unrescued" @@ -113,18 +113,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ServerId: serverId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUnrescueServerRequest { - return apiClient.UnrescueServer(ctx, model.ProjectId, model.ServerId) + return apiClient.DefaultAPI.UnrescueServer(ctx, model.ProjectId, model.Region, model.ServerId) } diff --git a/internal/cmd/server/unrescue/unrescue_test.go b/internal/cmd/server/unrescue/unrescue_test.go index 1f2d95712..72edc1bef 100644 --- a/internal/cmd/server/unrescue/unrescue_test.go +++ b/internal/cmd/server/unrescue/unrescue_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, ServerId: testServerId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUnrescueServerRequest)) iaas.ApiUnrescueServerRequest { - request := testClient.UnrescueServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.UnrescueServer(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -96,7 +99,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -104,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -128,54 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -199,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/update/update.go b/internal/cmd/server/update/update.go index bc6f821a1..1ce7a8e88 100644 --- a/internal/cmd/server/update/update.go +++ b/internal/cmd/server/update/update.go @@ -2,13 +2,13 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -32,10 +31,10 @@ type inputModel struct { *globalflags.GlobalFlagModel ServerId string Name *string - Labels *map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", serverIdArg), Short: "Updates a server", @@ -64,7 +63,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) serverLabel = model.ServerId @@ -72,12 +71,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update server %q?", serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update server %q?", serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,52 +108,27 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu GlobalFlagModel: globalFlags, Name: flags.FlagToStringPointer(p, cmd, nameFlag), ServerId: serverId, - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateServerRequest { - req := apiClient.UpdateServer(ctx, model.ProjectId, model.ServerId) + req := apiClient.DefaultAPI.UpdateServer(ctx, model.ProjectId, model.Region, model.ServerId) payload := iaas.UpdateServerPayload{ Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, } return req.UpdateServerPayload(payload) } func outputResult(p *print.Printer, outputFormat, serverLabel string, server *iaas.Server) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(server, "", " ") - if err != nil { - return fmt.Errorf("marshal server: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(server, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, server, func() error { p.Outputf("Updated server %q.\n", serverLabel) return nil - } + }) } diff --git a/internal/cmd/server/update/update_test.go b/internal/cmd/server/update/update_test.go index 63398311e..bd66b8572 100644 --- a/internal/cmd/server/update/update_test.go +++ b/internal/cmd/server/update/update_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() @@ -37,9 +38,11 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - nameFlag: "example-server-name", - projectIdFlag: testProjectId, - labelFlag: "key=value", + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + nameFlag: "example-server-name", + labelFlag: "key=value", } for _, mod := range mods { mod(flagValues) @@ -51,13 +54,14 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Name: utils.Ptr("example-server-name"), ServerId: testServerId, - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -66,7 +70,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateServerRequest)) iaas.ApiUpdateServerRequest { - request := testClient.UpdateServer(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.UpdateServer(testCtx, testProjectId, testRegion, testServerId) request = request.UpdateServerPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -77,9 +81,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiUpdateServerRequest)) iaas.Api func fixturePayload(mods ...func(payload *iaas.UpdateServerPayload)) iaas.UpdateServerPayload { payload := iaas.UpdateServerPayload{ Name: utils.Ptr("example-server-name"), - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(&payload) @@ -112,7 +116,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -120,7 +124,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -128,7 +132,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -163,7 +167,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ + model.Labels = map[string]any{ "key": "value", } }), @@ -172,8 +176,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -205,7 +209,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -243,7 +247,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -269,11 +273,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.server); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.server); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/volume/attach/attach.go b/internal/cmd/server/volume/attach/attach.go index 00217c513..97667419b 100644 --- a/internal/cmd/server/volume/attach/attach.go +++ b/internal/cmd/server/volume/attach/attach.go @@ -2,12 +2,13 @@ package attach import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -31,12 +31,12 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string VolumeId string DeleteOnTermination *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("attach %s", volumeIdArg), Short: "Attaches a volume to a server", @@ -65,26 +65,24 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to attach volume %q to server %q?", volumeLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to attach volume %q to server %q?", volumeLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -118,25 +116,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), DeleteOnTermination: flags.FlagToBoolPointer(p, cmd, deleteOnTerminationFlag), VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiAddVolumeToServerRequest { - req := apiClient.AddVolumeToServer(ctx, model.ProjectId, *model.ServerId, model.VolumeId) + req := apiClient.DefaultAPI.AddVolumeToServer(ctx, model.ProjectId, model.Region, model.ServerId, model.VolumeId) payload := iaas.AddVolumeToServerPayload{ DeleteOnTermination: model.DeleteOnTermination, } @@ -144,25 +134,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } func outputResult(p *print.Printer, outputFormat, volumeLabel, serverLabel string, volume iaas.VolumeAttachment) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volume, "", " ") - if err != nil { - return fmt.Errorf("marshal server volume: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volume, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, volume, func() error { p.Outputf("Attached volume %q to server %q\n", volumeLabel, serverLabel) return nil - } + }) } diff --git a/internal/cmd/server/volume/attach/attach_test.go b/internal/cmd/server/volume/attach/attach_test.go index 9a180a4c0..841a19fa1 100644 --- a/internal/cmd/server/volume/attach/attach_test.go +++ b/internal/cmd/server/volume/attach/attach_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -37,7 +38,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, deleteOnTerminationFlag: "true", } @@ -52,8 +55,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, VolumeId: testVolumeId, DeleteOnTermination: utils.Ptr(true), } @@ -74,7 +78,7 @@ func fixturePayload(mods ...func(payload *iaas.AddVolumeToServerPayload)) iaas.A } func fixtureRequest(mods ...func(request *iaas.ApiAddVolumeToServerRequest)) iaas.ApiAddVolumeToServerRequest { - request := testClient.AddVolumeToServer(testCtx, testProjectId, testServerId, testVolumeId) + request := testClient.DefaultAPI.AddVolumeToServer(testCtx, testProjectId, testRegion, testServerId, testVolumeId) request = request.AddVolumeToServerPayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -107,7 +111,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -115,7 +119,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -123,7 +127,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -171,8 +175,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -204,7 +208,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -242,7 +246,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -269,11 +273,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.volumeLabel, tt.args.serverLabel, tt.args.volume); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.volumeLabel, tt.args.serverLabel, tt.args.volume); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/volume/describe/describe.go b/internal/cmd/server/volume/describe/describe.go index be160a2ca..d7730cadb 100644 --- a/internal/cmd/server/volume/describe/describe.go +++ b/internal/cmd/server/volume/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -28,11 +28,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string VolumeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", volumeIdArg), Short: "Describes a server volume attachment", @@ -65,18 +65,18 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } // Call API @@ -109,46 +109,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetAttachedVolumeRequest { - req := apiClient.GetAttachedVolume(ctx, model.ProjectId, *model.ServerId, model.VolumeId) + req := apiClient.DefaultAPI.GetAttachedVolume(ctx, model.ProjectId, model.Region, model.ServerId, model.VolumeId) return req } func outputResult(p *print.Printer, outputFormat, serverLabel, volumeLabel string, volume iaas.VolumeAttachment) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volume, "", " ") - if err != nil { - return fmt.Errorf("marshal server volume: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volume, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, volume, func() error { table := tables.NewTable() table.AddRow("SERVER ID", utils.PtrString(volume.ServerId)) table.AddSeparator() @@ -169,5 +144,5 @@ func outputResult(p *print.Printer, outputFormat, serverLabel, volumeLabel strin return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/volume/describe/describe_test.go b/internal/cmd/server/volume/describe/describe_test.go index f2182f04c..37791b9b9 100644 --- a/internal/cmd/server/volume/describe/describe_test.go +++ b/internal/cmd/server/volume/describe/describe_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -37,8 +38,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -51,8 +54,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, VolumeId: testVolumeId, } for _, mod := range mods { @@ -62,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetAttachedVolumeRequest)) iaas.ApiGetAttachedVolumeRequest { - request := testClient.GetAttachedVolume(testCtx, testProjectId, testServerId, testVolumeId) + request := testClient.DefaultAPI.GetAttachedVolume(testCtx, testProjectId, testRegion, testServerId, testVolumeId) for _, mod := range mods { mod(&request) } @@ -94,7 +98,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -102,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -110,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -147,8 +151,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -180,7 +184,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -218,7 +222,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -245,11 +249,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.volumeLabel, tt.args.volume); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.volumeLabel, tt.args.volume); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/volume/detach/detach.go b/internal/cmd/server/volume/detach/detach.go index ebd4fb70d..713a3d205 100644 --- a/internal/cmd/server/volume/detach/detach.go +++ b/internal/cmd/server/volume/detach/detach.go @@ -4,8 +4,11 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -26,11 +28,11 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string VolumeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("detach %s", volumeIdArg), Short: "Detaches a volume from a server", @@ -55,26 +57,24 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to detach volume %q from server %q?", volumeLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to detach volume %q from server %q?", volumeLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -108,23 +108,15 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRemoveVolumeFromServerRequest { - req := apiClient.RemoveVolumeFromServer(ctx, model.ProjectId, *model.ServerId, model.VolumeId) + req := apiClient.DefaultAPI.RemoveVolumeFromServer(ctx, model.ProjectId, model.Region, model.ServerId, model.VolumeId) return req } diff --git a/internal/cmd/server/volume/detach/detach_test.go b/internal/cmd/server/volume/detach/detach_test.go index 4934e778d..3541c79c3 100644 --- a/internal/cmd/server/volume/detach/detach_test.go +++ b/internal/cmd/server/volume/detach/detach_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -37,8 +38,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -51,8 +54,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, VolumeId: testVolumeId, } for _, mod := range mods { @@ -62,7 +66,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiRemoveVolumeFromServerRequest)) iaas.ApiRemoveVolumeFromServerRequest { - request := testClient.RemoveVolumeFromServer(testCtx, testProjectId, testServerId, testVolumeId) + request := testClient.DefaultAPI.RemoveVolumeFromServer(testCtx, testProjectId, testRegion, testServerId, testVolumeId) for _, mod := range mods { mod(&request) } @@ -93,7 +97,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -101,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -109,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -146,8 +150,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -179,7 +183,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -217,7 +221,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/server/volume/list/list.go b/internal/cmd/server/volume/list/list.go index 1fab049e4..148bae43c 100644 --- a/internal/cmd/server/volume/list/list.go +++ b/internal/cmd/server/volume/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -27,10 +27,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all server volumes", @@ -44,9 +44,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all volumes for a server with ID "xxx" in JSON format`, "$ stackit server volumes list --server-id xxx --output-format json"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -57,12 +57,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } // Call API @@ -71,16 +71,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list server volumes: %w", err) } - volumes := *resp.Items - if len(volumes) == 0 { - params.Printer.Info("No volumes found for server %s\n", serverLabel) - return nil - } + + volumes := resp.GetItems() // get volume names var volumeNames []string for i := range volumes { - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, *volumes[i].VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *volumes[i].VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = "" @@ -102,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -110,45 +107,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListAttachedVolumesRequest { - req := apiClient.ListAttachedVolumes(ctx, model.ProjectId, *model.ServerId) + req := apiClient.DefaultAPI.ListAttachedVolumes(ctx, model.ProjectId, model.Region, model.ServerId) return req } func outputResult(p *print.Printer, outputFormat, serverLabel string, volumeNames []string, volumes []iaas.VolumeAttachment) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volumes, "", " ") - if err != nil { - return fmt.Errorf("marshal server volume list: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volumes, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server volume list: %w", err) + return p.OutputResult(outputFormat, volumes, func() error { + if len(volumes) == 0 { + p.Outputf("No volumes found for server %s\n", serverLabel) + return nil } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("SERVER ID", "SERVER NAME", "VOLUME ID", "VOLUME NAME") for i := range volumes { @@ -164,5 +140,5 @@ func outputResult(p *print.Printer, outputFormat, serverLabel string, volumeName return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/server/volume/list/list_test.go b/internal/cmd/server/volume/list/list_test.go index a337fc247..38b8cbd29 100644 --- a/internal/cmd/server/volume/list/list_test.go +++ b/internal/cmd/server/volume/list/list_test.go @@ -4,30 +4,33 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, - serverIdFlag: testServerId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + serverIdFlag: testServerId, } for _, mod := range mods { mod(flagValues) @@ -40,8 +43,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, } for _, mod := range mods { mod(model) @@ -50,7 +54,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListAttachedVolumesRequest)) iaas.ApiListAttachedVolumesRequest { - request := testClient.ListAttachedVolumes(testCtx, testProjectId, testServerId) + request := testClient.DefaultAPI.ListAttachedVolumes(testCtx, testProjectId, testRegion, testServerId) for _, mod := range mods { mod(&request) } @@ -60,6 +64,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListAttachedVolumesRequest)) i func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -78,21 +83,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -121,46 +126,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -184,7 +150,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -220,11 +186,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serverLabel, tt.args.volumeNames, tt.args.volumes); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serverLabel, tt.args.volumeNames, tt.args.volumes); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/volume/update/update.go b/internal/cmd/server/volume/update/update.go index 3e2e4ae6b..61d74ac32 100644 --- a/internal/cmd/server/volume/update/update.go +++ b/internal/cmd/server/volume/update/update.go @@ -2,12 +2,13 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -31,12 +31,12 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - ServerId *string + ServerId string VolumeId string DeleteOnTermination *bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", volumeIdArg), Short: "Updates an attached volume of a server", @@ -61,26 +61,24 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - serverLabel, err := iaasUtils.GetServerName(ctx, apiClient, model.ProjectId, *model.ServerId) + serverLabel, err := iaasUtils.GetServerName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ServerId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get server name: %v", err) - serverLabel = *model.ServerId + serverLabel = model.ServerId } else if serverLabel == "" { - serverLabel = *model.ServerId + serverLabel = model.ServerId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update attached volume %q of server %q?", volumeLabel, serverLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update attached volume %q of server %q?", volumeLabel, serverLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -114,25 +112,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - ServerId: flags.FlagToStringPointer(p, cmd, serverIdFlag), + ServerId: flags.FlagToStringValue(p, cmd, serverIdFlag), DeleteOnTermination: flags.FlagToBoolPointer(p, cmd, deleteOnTerminationFlag), VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateAttachedVolumeRequest { - req := apiClient.UpdateAttachedVolume(ctx, model.ProjectId, *model.ServerId, model.VolumeId) + req := apiClient.DefaultAPI.UpdateAttachedVolume(ctx, model.ProjectId, model.Region, model.ServerId, model.VolumeId) payload := iaas.UpdateAttachedVolumePayload{ DeleteOnTermination: model.DeleteOnTermination, } @@ -140,25 +130,8 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } func outputResult(p *print.Printer, outputFormat, volumeLabel, serverLabel string, volume iaas.VolumeAttachment) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volume, "", " ") - if err != nil { - return fmt.Errorf("marshal server volume: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volume, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal server volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, volume, func() error { p.Outputf("Updated attached volume %q of server %q\n", volumeLabel, serverLabel) return nil - } + }) } diff --git a/internal/cmd/server/volume/update/update_test.go b/internal/cmd/server/volume/update/update_test.go index 1f820cff2..6cbd5205b 100644 --- a/internal/cmd/server/volume/update/update_test.go +++ b/internal/cmd/server/volume/update/update_test.go @@ -4,23 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testServerId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -37,7 +38,9 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + serverIdFlag: testServerId, deleteOnTerminationFlag: "true", } @@ -52,8 +55,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, - ServerId: utils.Ptr(testServerId), + ServerId: testServerId, VolumeId: testVolumeId, DeleteOnTermination: utils.Ptr(true), } @@ -74,7 +78,7 @@ func fixturePayload(mods ...func(payload *iaas.UpdateAttachedVolumePayload)) iaa } func fixtureRequest(mods ...func(request *iaas.ApiUpdateAttachedVolumeRequest)) iaas.ApiUpdateAttachedVolumeRequest { - request := testClient.UpdateAttachedVolume(testCtx, testProjectId, testServerId, testVolumeId) + request := testClient.DefaultAPI.UpdateAttachedVolume(testCtx, testProjectId, testRegion, testServerId, testVolumeId) request = request.UpdateAttachedVolumePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -106,7 +110,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -114,7 +118,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -122,7 +126,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -170,8 +174,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -203,7 +207,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -241,7 +245,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -268,11 +272,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.volumeLabel, tt.args.serverLabel, tt.args.volume); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.volumeLabel, tt.args.serverLabel, tt.args.volume); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/server/volume/volume.go b/internal/cmd/server/volume/volume.go index 15dd14ef7..444ef040b 100644 --- a/internal/cmd/server/volume/volume.go +++ b/internal/cmd/server/volume/volume.go @@ -1,19 +1,19 @@ package volume import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/server/volume/attach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/volume/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/server/volume/detach" "github.com/stackitcloud/stackit-cli/internal/cmd/server/volume/list" "github.com/stackitcloud/stackit-cli/internal/cmd/server/volume/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "volume", Short: "Provides functionality for server volumes", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(attach.NewCmd(params)) cmd.AddCommand(detach.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) diff --git a/internal/cmd/service-account/create/create.go b/internal/cmd/service-account/create/create.go index c998de0f8..8d9daf092 100644 --- a/internal/cmd/service-account/create/create.go +++ b/internal/cmd/service-account/create/create.go @@ -2,11 +2,10 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -30,7 +29,7 @@ type inputModel struct { Name *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a service account", @@ -41,9 +40,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a service account with name "my-service-account"`, "$ stackit service-account create --name my-service-account"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -60,12 +59,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a service account for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a service account for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -89,7 +86,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -100,15 +97,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Name: flags.FlagToStringPointer(p, cmd, nameFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -125,25 +114,8 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, serviceAc return fmt.Errorf("service account is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(serviceAccount, "", " ") - if err != nil { - return fmt.Errorf("marshal service account: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(serviceAccount, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal service account: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, serviceAccount, func() error { p.Outputf("Created service account for project %q. Email: %s\n", projectLabel, utils.PtrString(serviceAccount.Email)) return nil - } + }) } diff --git a/internal/cmd/service-account/create/create_test.go b/internal/cmd/service-account/create/create_test.go index 1c3fac895..29882d1c7 100644 --- a/internal/cmd/service-account/create/create_test.go +++ b/internal/cmd/service-account/create/create_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -62,6 +62,7 @@ func fixtureRequest(mods ...func(request *serviceaccount.ApiCreateServiceAccount func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -117,46 +118,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -213,11 +175,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.serviceAccount); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.serviceAccount); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/delete/delete.go b/internal/cmd/service-account/delete/delete.go index a92acdb4f..eefc8a3e3 100644 --- a/internal/cmd/service-account/delete/delete.go +++ b/internal/cmd/service-account/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -25,7 +26,7 @@ type inputModel struct { Email string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", emailArg), Short: "Deletes a service account", @@ -49,12 +50,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete service account %s? (This cannot be undone)", model.Email) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete service account %s? (This cannot be undone)", model.Email) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -87,15 +86,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Email: email, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/delete/delete_test.go b/internal/cmd/service-account/delete/delete_test.go index 9a0dc74ef..7dbcc6dfd 100644 --- a/internal/cmd/service-account/delete/delete_test.go +++ b/internal/cmd/service-account/delete/delete_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -126,54 +125,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/service-account/get-jwks/get_jwks.go b/internal/cmd/service-account/get-jwks/get_jwks.go index 441851bac..340df926f 100644 --- a/internal/cmd/service-account/get-jwks/get_jwks.go +++ b/internal/cmd/service-account/get-jwks/get_jwks.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/print" @@ -23,7 +24,7 @@ type inputModel struct { Email string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("get-jwks %s", emailArg), Short: "Shows the JWKS for a service account", @@ -73,15 +74,7 @@ func parseInput(p *print.Printer, _ *cobra.Command, inputArgs []string) (*inputM Email: email, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/get-jwks/get_jwks_test.go b/internal/cmd/service-account/get-jwks/get_jwks_test.go index c418ffb22..7798af6c5 100644 --- a/internal/cmd/service-account/get-jwks/get_jwks_test.go +++ b/internal/cmd/service-account/get-jwks/get_jwks_test.go @@ -4,13 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" ) type testCtxKey struct{} @@ -69,8 +69,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -92,7 +92,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -168,11 +168,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.serviceAccounts); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.serviceAccounts); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/key/create/create.go b/internal/cmd/service-account/key/create/create.go index 1b58d12b8..49bdd3918 100644 --- a/internal/cmd/service-account/key/create/create.go +++ b/internal/cmd/service-account/key/create/create.go @@ -6,7 +6,8 @@ import ( "fmt" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -34,7 +35,7 @@ type inputModel struct { PublicKey *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a service account key", @@ -55,9 +56,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a key for the service account with email "my-service-account-1234567@sa.stackit.cloud" and provide the public key in a .pem file"`, `$ stackit service-account key create --email my-service-account-1234567@sa.stackit.cloud --public-key @./public.pem`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -68,16 +69,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - validUntilInfo := "The key will be valid until deleted" - if model.ExpiresInDays != nil { - validUntilInfo = fmt.Sprintf("The key will be valid for %d days", *model.ExpiresInDays) - } - prompt := fmt.Sprintf("Are you sure you want to create a key for service account %s? %s", model.ServiceAccountEmail, validUntilInfo) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + validUntilInfo := "The key will be valid until deleted" + if model.ExpiresInDays != nil { + validUntilInfo = fmt.Sprintf("The key will be valid for %d days", *model.ExpiresInDays) + } + prompt := fmt.Sprintf("Are you sure you want to create a key for service account %s? %s", model.ServiceAccountEmail, validUntilInfo) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,7 +110,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -140,15 +139,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { PublicKey: flags.FlagToStringPointer(p, cmd, publicKeyFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/key/create/create_test.go b/internal/cmd/service-account/key/create/create_test.go index 8f07ed01f..3ee9c7739 100644 --- a/internal/cmd/service-account/key/create/create_test.go +++ b/internal/cmd/service-account/key/create/create_test.go @@ -5,9 +5,8 @@ import ( "testing" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -65,6 +64,7 @@ func fixtureRequest(mods ...func(request *serviceaccount.ApiCreateServiceAccount func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -142,46 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/service-account/key/delete/delete.go b/internal/cmd/service-account/key/delete/delete.go index e780ddd33..c468d72a7 100644 --- a/internal/cmd/service-account/key/delete/delete.go +++ b/internal/cmd/service-account/key/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -31,7 +32,7 @@ type inputModel struct { KeyId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", keyIdArg), Short: "Deletes a service account key", @@ -55,12 +56,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete the key %s from service account %s?", model.KeyId, model.ServiceAccountEmail) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete the key %s from service account %s?", model.KeyId, model.ServiceAccountEmail) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -108,15 +107,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu KeyId: keyId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/key/delete/delete_test.go b/internal/cmd/service-account/key/delete/delete_test.go index 9e811b0a6..7f4ade070 100644 --- a/internal/cmd/service-account/key/delete/delete_test.go +++ b/internal/cmd/service-account/key/delete/delete_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -149,54 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/service-account/key/describe/describe.go b/internal/cmd/service-account/key/describe/describe.go index ecf541f3f..84fb62dd8 100644 --- a/internal/cmd/service-account/key/describe/describe.go +++ b/internal/cmd/service-account/key/describe/describe.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -32,7 +33,7 @@ type inputModel struct { KeyId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", keyIdArg), Short: "Shows details of a service account key", @@ -98,15 +99,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu KeyId: keyId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/key/describe/describe_test.go b/internal/cmd/service-account/key/describe/describe_test.go index 7111fa0a6..af54260bc 100644 --- a/internal/cmd/service-account/key/describe/describe_test.go +++ b/internal/cmd/service-account/key/describe/describe_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -149,54 +149,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -253,11 +206,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.key); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.key); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/key/key.go b/internal/cmd/service-account/key/key.go index b91d31b6a..36d982f4e 100644 --- a/internal/cmd/service-account/key/key.go +++ b/internal/cmd/service-account/key/key.go @@ -1,19 +1,19 @@ package key import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/key/create" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/key/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/key/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/key/list" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/key/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "key", Short: "Provides functionality for service account keys", @@ -25,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/service-account/key/list/list.go b/internal/cmd/service-account/key/list/list.go index a39782f56..2cb1d3288 100644 --- a/internal/cmd/service-account/key/list/list.go +++ b/internal/cmd/service-account/key/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-account/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) const ( @@ -32,7 +32,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all service account keys", @@ -49,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 keys belonging to the service account with email "my-service-account-1234567@sa.stackit.cloud"`, "$ stackit service-account key list --email my-service-account-1234567@sa.stackit.cloud --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -95,7 +95,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,15 +123,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -141,24 +133,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *serviceacco } func outputResult(p *print.Printer, outputFormat string, keys []serviceaccount.ServiceAccountKeyListResponse) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(keys, "", " ") - if err != nil { - return fmt.Errorf("marshal keys metadata: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(keys, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal keys metadata: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, keys, func() error { table := tables.NewTable() table.SetHeader("ID", "ACTIVE", "CREATED_AT", "VALID_UNTIL") for i := range keys { @@ -180,5 +155,5 @@ func outputResult(p *print.Printer, outputFormat string, keys []serviceaccount.S } return nil - } + }) } diff --git a/internal/cmd/service-account/key/list/list_test.go b/internal/cmd/service-account/key/list/list_test.go index ebde67439..3f5200da3 100644 --- a/internal/cmd/service-account/key/list/list_test.go +++ b/internal/cmd/service-account/key/list/list_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) @@ -63,6 +62,7 @@ func fixtureRequest(mods ...func(request *serviceaccount.ApiListServiceAccountKe func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -124,48 +124,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -228,11 +187,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.keys); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.keys); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/key/update/update.go b/internal/cmd/service-account/key/update/update.go index 62183d56f..5594396e4 100644 --- a/internal/cmd/service-account/key/update/update.go +++ b/internal/cmd/service-account/key/update/update.go @@ -6,7 +6,8 @@ import ( "fmt" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -39,7 +40,7 @@ type inputModel struct { Deactivate bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", keyIdArg), Short: "Updates a service account key", @@ -72,12 +73,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update the key with ID %q?", model.KeyId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update the key with ID %q?", model.KeyId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -149,15 +148,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Deactivate: deactivate, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/key/update/update_test.go b/internal/cmd/service-account/key/update/update_test.go index 84dde9c6d..2d2c66bfa 100644 --- a/internal/cmd/service-account/key/update/update_test.go +++ b/internal/cmd/service-account/key/update/update_test.go @@ -5,9 +5,8 @@ import ( "testing" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -196,54 +195,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/service-account/list/list.go b/internal/cmd/service-account/list/list.go index 121b5216d..692ea90c8 100644 --- a/internal/cmd/service-account/list/list.go +++ b/internal/cmd/service-account/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-account/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all service accounts", @@ -41,9 +41,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List all service accounts`, "$ stackit service-account list"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -88,7 +88,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -107,15 +107,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -125,20 +117,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *serviceacco } func outputResult(p *print.Printer, outputFormat string, serviceAccounts []serviceaccount.ServiceAccount) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(serviceAccounts, "", " ") - if err != nil { - return fmt.Errorf("marshal service accounts list: %w", err) - } - p.Outputln(string(details)) - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(serviceAccounts, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal service accounts list: %w", err) - } - p.Outputln(string(details)) - default: + return p.OutputResult(outputFormat, serviceAccounts, func() error { table := tables.NewTable() table.SetHeader("ID", "EMAIL") for i := range serviceAccounts { @@ -152,7 +131,6 @@ func outputResult(p *print.Printer, outputFormat string, serviceAccounts []servi if err != nil { return fmt.Errorf("render table: %w", err) } - } - - return nil + return nil + }) } diff --git a/internal/cmd/service-account/list/list_test.go b/internal/cmd/service-account/list/list_test.go index 161f050e2..2ce3d2659 100644 --- a/internal/cmd/service-account/list/list_test.go +++ b/internal/cmd/service-account/list/list_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) @@ -60,6 +59,7 @@ func fixtureRequest(mods ...func(request *serviceaccount.ApiListServiceAccountsR func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -114,48 +114,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -218,11 +177,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serviceAccounts); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serviceAccounts); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/service_account.go b/internal/cmd/service-account/service_account.go index 9c2462bfa..856be2fba 100644 --- a/internal/cmd/service-account/service_account.go +++ b/internal/cmd/service-account/service_account.go @@ -1,7 +1,6 @@ package serviceaccount import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/create" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/delete" getjwks "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/get-jwks" @@ -9,12 +8,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/list" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/token" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "service-account", Short: "Provides functionality for service accounts", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/service-account/token/create/create.go b/internal/cmd/service-account/token/create/create.go index 5b74fdae7..c5e8f9a18 100644 --- a/internal/cmd/service-account/token/create/create.go +++ b/internal/cmd/service-account/token/create/create.go @@ -2,12 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-account/client" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { TTLDays *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates an access token for a service account", @@ -51,9 +51,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create an access token for the service account with email "my-service-account-1234567@sa.stackit.cloud" with a time to live of 10 days`, "$ stackit service-account token create --email my-service-account-1234567@sa.stackit.cloud --ttl-days 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -64,12 +64,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create an access token for service account %s?", model.ServiceAccountEmail) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create an access token for service account %s?", model.ServiceAccountEmail) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -95,7 +93,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,15 +121,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { TTLDays: &ttlDays, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -148,27 +138,10 @@ func outputResult(p *print.Printer, outputFormat, serviceAccountEmail string, to return fmt.Errorf("token is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(token, "", " ") - if err != nil { - return fmt.Errorf("marshal service account access token: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(token, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal service account access token: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, token, func() error { p.Outputf("Created access token for service account %s. Token ID: %s\n\n", serviceAccountEmail, utils.PtrString(token.Id)) p.Outputf("Valid until: %s\n", utils.PtrString(token.ValidUntil)) p.Outputf("Token: %s\n", utils.PtrString(token.Token)) return nil - } + }) } diff --git a/internal/cmd/service-account/token/create/create_test.go b/internal/cmd/service-account/token/create/create_test.go index ffa760ce5..7a7354204 100644 --- a/internal/cmd/service-account/token/create/create_test.go +++ b/internal/cmd/service-account/token/create/create_test.go @@ -4,9 +4,9 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -67,6 +67,7 @@ func fixtureRequest(mods ...func(request *serviceaccount.ApiCreateAccessTokenReq func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -121,46 +122,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -219,11 +181,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.serviceAccountEmail, tt.args.token); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.serviceAccountEmail, tt.args.token); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/token/list/list.go b/internal/cmd/service-account/token/list/list.go index 956b79908..436d599f6 100644 --- a/internal/cmd/service-account/token/list/list.go +++ b/internal/cmd/service-account/token/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -33,7 +32,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists access tokens of a service account", @@ -54,9 +53,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 access tokens of the service account with email "my-service-account-1234567@sa.stackit.cloud"`, "$ stackit service-account token list --email my-service-account-1234567@sa.stackit.cloud --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -100,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -128,15 +127,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: limit, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } @@ -146,24 +137,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *serviceacco } func outputResult(p *print.Printer, outputFormat string, tokensMetadata []serviceaccount.AccessTokenMetadata) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(tokensMetadata, "", " ") - if err != nil { - return fmt.Errorf("marshal tokens metadata: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(tokensMetadata, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal tokens metadata: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, tokensMetadata, func() error { table := tables.NewTable() table.SetHeader("ID", "ACTIVE", "CREATED_AT", "VALID_UNTIL") for i := range tokensMetadata { @@ -181,5 +155,5 @@ func outputResult(p *print.Printer, outputFormat string, tokensMetadata []servic } return nil - } + }) } diff --git a/internal/cmd/service-account/token/list/list_test.go b/internal/cmd/service-account/token/list/list_test.go index e03ee657f..3cd500caa 100644 --- a/internal/cmd/service-account/token/list/list_test.go +++ b/internal/cmd/service-account/token/list/list_test.go @@ -4,15 +4,14 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) @@ -63,6 +62,7 @@ func fixtureRequest(mods ...func(request *serviceaccount.ApiListAccessTokensRequ func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -124,48 +124,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -228,11 +187,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.tokensMetadata); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.tokensMetadata); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/service-account/token/revoke/revoke.go b/internal/cmd/service-account/token/revoke/revoke.go index 0ac624a89..2a892e005 100644 --- a/internal/cmd/service-account/token/revoke/revoke.go +++ b/internal/cmd/service-account/token/revoke/revoke.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -30,7 +31,7 @@ type inputModel struct { TokenId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("revoke %s", tokenIdArg), Short: "Revokes an access token of a service account", @@ -58,12 +59,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to revoke the access token with ID %q?", model.TokenId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to revoke the access token with ID %q?", model.TokenId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -111,15 +110,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu TokenId: tokenId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } diff --git a/internal/cmd/service-account/token/revoke/revoke_test.go b/internal/cmd/service-account/token/revoke/revoke_test.go index 17387aceb..cebb61897 100644 --- a/internal/cmd/service-account/token/revoke/revoke_test.go +++ b/internal/cmd/service-account/token/revoke/revoke_test.go @@ -4,9 +4,8 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -149,54 +148,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } diff --git a/internal/cmd/service-account/token/token.go b/internal/cmd/service-account/token/token.go index ce4b4eac6..5faeff394 100644 --- a/internal/cmd/service-account/token/token.go +++ b/internal/cmd/service-account/token/token.go @@ -1,17 +1,17 @@ package token import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/token/create" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/token/list" "github.com/stackitcloud/stackit-cli/internal/cmd/service-account/token/revoke" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "token", Short: "Provides functionality for service account tokens", @@ -23,7 +23,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(revoke.NewCmd(params)) diff --git a/internal/cmd/ske/cluster/cluster.go b/internal/cmd/ske/cluster/cluster.go index 87994c2fa..e4dd6bcbb 100644 --- a/internal/cmd/ske/cluster/cluster.go +++ b/internal/cmd/ske/cluster/cluster.go @@ -1,7 +1,6 @@ package cluster import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/cluster/create" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/cluster/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/cluster/describe" @@ -13,12 +12,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/ske/cluster/update" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/cluster/wakeup" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "cluster", Short: "Provides functionality for SKE cluster", @@ -30,7 +30,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(generatepayload.NewCmd(params)) cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) diff --git a/internal/cmd/ske/cluster/create/create.go b/internal/cmd/ske/cluster/create/create.go index af9ba2336..92c761656 100644 --- a/internal/cmd/ske/cluster/create/create.go +++ b/internal/cmd/ske/cluster/create/create.go @@ -5,9 +5,12 @@ import ( "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,8 +24,6 @@ import ( skeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" ) const ( @@ -37,7 +38,7 @@ type inputModel struct { Payload *ske.CreateOrUpdateClusterPayload } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("create %s", clusterNameArg), Short: "Creates a SKE cluster", @@ -82,35 +83,32 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a cluster for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } - } - - // Configure ServiceEnable API client - serviceEnablementApiClient, err := serviceEnablementClient.ConfigureClient(params.Printer, params.CliVersion) + prompt := fmt.Sprintf("Are you sure you want to create a cluster for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) if err != nil { return err } - // Check if the project is enabled before trying to create - enabled, err := serviceEnablementUtils.ProjectEnabled(ctx, serviceEnablementApiClient, model.ProjectId, model.Region) + // Configure ServiceEnable API client + serviceEnablementApiClient, err := serviceEnablementClient.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } - if !enabled { - return &errors.ServiceDisabledError{ - Service: "ske", - } - } // Check if cluster exists - exists, err := skeUtils.ClusterExists(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName) + exists, err := skeUtils.ClusterExists(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName) if err != nil { - return err + // Check if the project is enabled + enabled, enabledErr := serviceEnablementUtils.ProjectEnabled(ctx, serviceEnablementApiClient.DefaultAPI, model.ProjectId, model.Region) + if enabledErr != nil { + return fmt.Errorf("check if project is enabled failed: %w", enabledErr) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "ske", + } + } + return fmt.Errorf("cluster exists check failed: %w", err) } if exists { return fmt.Errorf("cluster with name %s already exists", model.ClusterName) @@ -118,7 +116,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Fill in default payload, if needed if model.Payload == nil { - defaultPayload, err := skeUtils.GetDefaultPayload(ctx, apiClient, model.Region) + defaultPayload, err := skeUtils.GetDefaultPayload(ctx, apiClient.DefaultAPI, model.Region) if err != nil { return fmt.Errorf("get default payload: %w", err) } @@ -135,13 +133,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating cluster") - _, err = wait.CreateOrUpdateClusterWaitHandler(ctx, apiClient, model.ProjectId, model.Region, name).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating cluster", func() error { + _, err = wait.CreateOrUpdateClusterWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, name).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, projectLabel, resp) @@ -179,20 +177,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Payload: payload, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiCreateOrUpdateClusterRequest { - req := apiClient.CreateOrUpdateCluster(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.CreateOrUpdateCluster(ctx, model.ProjectId, model.Region, model.ClusterName) req = req.CreateOrUpdateClusterPayload(*model.Payload) return req @@ -203,29 +193,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, projectLabe return fmt.Errorf("cluster is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(cluster, "", " ") - if err != nil { - return fmt.Errorf("marshal SKE cluster: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(cluster, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SKE cluster: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, cluster, func() error { operationState := "Created" if async { operationState = "Triggered creation of" } p.Outputf("%s cluster for project %q. Cluster name: %s\n", operationState, projectLabel, utils.PtrString(cluster.Name)) return nil - } + }) } diff --git a/internal/cmd/ske/cluster/create/create_test.go b/internal/cmd/ske/cluster/create/create_test.go index 8fba70614..ec40626c2 100644 --- a/internal/cmd/ske/cluster/create/create_test.go +++ b/internal/cmd/ske/cluster/create/create_test.go @@ -2,19 +2,19 @@ package create import ( "context" - "fmt" "testing" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -22,53 +22,67 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" const testRegion = "eu01" var testPayload = &ske.CreateOrUpdateClusterPayload{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("1.25.15"), + Kubernetes: ske.Kubernetes{ + Version: "1.25.15", + AdditionalProperties: map[string]any{}, }, - Nodepools: &[]ske.Nodepool{ + Nodepools: []ske.Nodepool{ { - Name: utils.Ptr("np-name"), - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("flatcar"), - Version: utils.Ptr("3760.2.1"), + Name: "np-name", + Machine: ske.Machine{ + Image: ske.Image{ + Name: "flatcar", + Version: "3760.2.1", + AdditionalProperties: map[string]any{}, }, - Type: utils.Ptr("b1.2"), + Type: "b1.2", + AdditionalProperties: map[string]any{}, + }, + Minimum: int32(1), + Maximum: int32(2), + MaxSurge: utils.Ptr(int32(1)), + Volume: ske.Volume{ + Type: utils.Ptr("storage_premium_perf0"), + Size: int32(40), + AdditionalProperties: map[string]any{}, }, - Minimum: utils.Ptr(int64(1)), - Maximum: utils.Ptr(int64(2)), - MaxSurge: utils.Ptr(int64(1)), - Volume: &ske.Volume{ - Type: utils.Ptr("storage_premium_perf0"), - Size: utils.Ptr(int64(40)), + AvailabilityZones: []string{"eu01-3"}, + Cri: &ske.CRI{ + Name: utils.Ptr("containerd"), + AdditionalProperties: map[string]any{}, }, - AvailabilityZones: &[]string{"eu01-3"}, - Cri: &ske.CRI{Name: ske.CRINAME_DOCKER.Ptr()}, + AdditionalProperties: map[string]any{}, }, }, Extensions: &ske.Extension{ Acl: &ske.ACL{ - Enabled: utils.Ptr(true), - AllowedCidrs: &[]string{"0.0.0.0/0"}, + Enabled: true, + AllowedCidrs: []string{"0.0.0.0/0"}, + AdditionalProperties: map[string]any{}, }, + AdditionalProperties: map[string]any{}, }, Maintenance: &ske.Maintenance{ - AutoUpdate: &ske.MaintenanceAutoUpdate{ - KubernetesVersion: utils.Ptr(true), - MachineImageVersion: utils.Ptr(true), + AutoUpdate: ske.MaintenanceAutoUpdate{ + KubernetesVersion: utils.Ptr(true), + MachineImageVersion: utils.Ptr(true), + AdditionalProperties: map[string]any{}, }, - TimeWindow: &ske.TimeWindow{ - End: utils.Ptr(time.Date(0, 1, 1, 5, 0, 0, 0, time.FixedZone("test-zone", 2*60*60))), - Start: utils.Ptr(time.Date(0, 1, 1, 3, 0, 0, 0, time.FixedZone("test-zone", 2*60*60))), + TimeWindow: ske.TimeWindow{ + End: time.Date(0, 1, 1, 5, 0, 0, 0, time.FixedZone("test-zone", 2*60*60)), + Start: time.Date(0, 1, 1, 3, 0, 0, 0, time.FixedZone("test-zone", 2*60*60)), + AdditionalProperties: map[string]any{}, }, + AdditionalProperties: map[string]any{}, }, + AdditionalProperties: map[string]any{}, } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -85,8 +99,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, - payloadFlag: fmt.Sprintf(`{ - "name": "cli-jp", + payloadFlag: `{ "kubernetes": { "version": "1.25.15" }, @@ -104,7 +117,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st "maximum": 2, "maxSurge": 1, "volume": { "type": "storage_premium_perf0", "size": 40 }, - "cri": { "name": "%s" }, + "cri": { "name": "containerd" }, "availabilityZones": ["eu01-3"] } ], @@ -119,7 +132,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st "start": "0000-01-01T03:00:00+02:00" } } - }`, ske.CRINAME_DOCKER), + }`, } for _, mod := range mods { mod(flagValues) @@ -144,7 +157,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiCreateOrUpdateClusterRequest)) ske.ApiCreateOrUpdateClusterRequest { - request := testClient.CreateOrUpdateCluster(testCtx, testProjectId, testRegion, fixtureInputModel().ClusterName) + request := testClient.DefaultAPI.CreateOrUpdateCluster(testCtx, testProjectId, testRegion, fixtureInputModel().ClusterName) request = request.CreateOrUpdateClusterPayload(*testPayload) for _, mod := range mods { mod(&request) @@ -233,62 +246,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -314,6 +272,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -347,11 +306,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.cluster); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.projectLabel, tt.args.cluster); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/cluster/delete/delete.go b/internal/cmd/ske/cluster/delete/delete.go index 12068d9e3..f8833c202 100644 --- a/internal/cmd/ske/cluster/delete/delete.go +++ b/internal/cmd/ske/cluster/delete/delete.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", clusterNameArg), Short: "Deletes a SKE cluster", @@ -51,12 +52,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete cluster %q? (This cannot be undone)", model.ClusterName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete cluster %q? (This cannot be undone)", model.ClusterName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -68,13 +67,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting cluster") - _, err = wait.DeleteClusterWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting cluster", func() error { + _, err = wait.DeleteClusterWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -101,19 +100,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiDeleteClusterRequest { - req := apiClient.DeleteCluster(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.DeleteCluster(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/cluster/delete/delete_test.go b/internal/cmd/ske/cluster/delete/delete_test.go index 2835c477b..182af1949 100644 --- a/internal/cmd/ske/cluster/delete/delete_test.go +++ b/internal/cmd/ske/cluster/delete/delete_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiDeleteClusterRequest)) ske.ApiDeleteClusterRequest { - request := testClient.DeleteCluster(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.DeleteCluster(testCtx, testProjectId, testRegion, testClusterName) for _, mod := range mods { mod(&request) } @@ -130,54 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -204,6 +156,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/ske/cluster/describe/describe.go b/internal/cmd/ske/cluster/describe/describe.go index a983f442e..bde8763f7 100644 --- a/internal/cmd/ske/cluster/describe/describe.go +++ b/internal/cmd/ske/cluster/describe/describe.go @@ -2,13 +2,14 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" ) const ( @@ -29,7 +29,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", clusterNameArg), Short: "Shows details of a SKE cluster", @@ -81,20 +81,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiGetClusterRequest { - req := apiClient.GetCluster(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.GetCluster(ctx, model.ProjectId, model.Region, model.ClusterName) return req } @@ -103,27 +95,10 @@ func outputResult(p *print.Printer, outputFormat string, cluster *ske.Cluster) e return fmt.Errorf("cluster is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(cluster, "", " ") - if err != nil { - return fmt.Errorf("marshal SKE cluster: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(cluster, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SKE cluster: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, cluster, func() error { acl := []string{} if cluster.Extensions != nil && cluster.Extensions.Acl != nil { - acl = *cluster.Extensions.Acl.AllowedCidrs + acl = cluster.Extensions.Acl.AllowedCidrs } table := tables.NewTable() @@ -136,8 +111,8 @@ func outputResult(p *print.Printer, outputFormat string, cluster *ske.Cluster) e handleClusterErrors(clusterErrs, &table) } } - if cluster.Kubernetes != nil { - table.AddRow("VERSION", utils.PtrString(cluster.Kubernetes.Version)) + if cluster.Kubernetes.Version != "" { + table.AddRow("VERSION", cluster.Kubernetes.Version) table.AddSeparator() } @@ -148,7 +123,7 @@ func outputResult(p *print.Printer, outputFormat string, cluster *ske.Cluster) e } return nil - } + }) } func handleClusterErrors(clusterErrs []ske.ClusterError, table *tables.Table) { @@ -157,7 +132,7 @@ func handleClusterErrors(clusterErrs []ske.ClusterError, table *tables.Table) { b := new(strings.Builder) fmt.Fprint(b, e.GetCode()) if msg, ok := e.GetMessageOk(); ok { - fmt.Fprintf(b, ": %s", msg) + fmt.Fprintf(b, ": %s", *msg) } errs = append(errs, b.String()) } diff --git a/internal/cmd/ske/cluster/describe/describe_test.go b/internal/cmd/ske/cluster/describe/describe_test.go index a5f7f766e..1a4aacbb8 100644 --- a/internal/cmd/ske/cluster/describe/describe_test.go +++ b/internal/cmd/ske/cluster/describe/describe_test.go @@ -4,15 +4,16 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +21,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" @@ -63,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiGetClusterRequest)) ske.ApiGetClusterRequest { - request := testClient.GetCluster(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.GetCluster(testCtx, testProjectId, testRegion, testClusterName) for _, mod := range mods { mod(&request) } @@ -131,54 +132,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -205,6 +159,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -242,7 +197,7 @@ func TestOutputResult(t *testing.T) { cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_INFRA_SNA_NETWORK_NOT_FOUND"), Message: utils.Ptr("Network configuration not found"), @@ -260,7 +215,7 @@ func TestOutputResult(t *testing.T) { cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_INFRA_SNA_NETWORK_NOT_FOUND"), Message: utils.Ptr("Network configuration not found"), @@ -286,7 +241,7 @@ func TestOutputResult(t *testing.T) { cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_FETCHING_ERRORS_NOT_POSSIBLE"), }, @@ -316,7 +271,7 @@ func TestOutputResult(t *testing.T) { cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{}, + Errors: []ske.ClusterError{}, }, }, }, @@ -339,7 +294,7 @@ func TestOutputResult(t *testing.T) { cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_INFRA_SNA_NETWORK_NOT_FOUND"), Message: utils.Ptr("Network configuration not found"), @@ -357,7 +312,7 @@ func TestOutputResult(t *testing.T) { cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_INFRA_SNA_NETWORK_NOT_FOUND"), Message: utils.Ptr("Network configuration not found"), @@ -374,11 +329,11 @@ func TestOutputResult(t *testing.T) { outputFormat: "", cluster: &ske.Cluster{ Name: utils.Ptr("test-cluster"), - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("1.28.0"), + Kubernetes: ske.Kubernetes{ + Version: "1.28.0", }, Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_INFRA_SNA_NETWORK_NOT_FOUND"), Message: utils.Ptr("Network configuration not found"), @@ -397,12 +352,12 @@ func TestOutputResult(t *testing.T) { Name: utils.Ptr("test-cluster"), Extensions: &ske.Extension{ Acl: &ske.ACL{ - AllowedCidrs: &[]string{"10.0.0.0/8"}, - Enabled: utils.Ptr(true), + AllowedCidrs: []string{"10.0.0.0/8"}, + Enabled: true, }, }, Status: &ske.ClusterStatus{ - Errors: &[]ske.ClusterError{ + Errors: []ske.ClusterError{ { Code: utils.Ptr("SKE_INFRA_SNA_NETWORK_NOT_FOUND"), Message: utils.Ptr("Network configuration not found"), @@ -414,11 +369,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.cluster); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.cluster); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/cluster/generate-payload/generate_payload.go b/internal/cmd/ske/cluster/generate-payload/generate_payload.go index 566e51f33..7ff0a87d3 100644 --- a/internal/cmd/ske/cluster/generate-payload/generate_payload.go +++ b/internal/cmd/ske/cluster/generate-payload/generate_payload.go @@ -5,7 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,7 @@ import ( skeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) const ( @@ -31,7 +32,7 @@ type inputModel struct { FilePath *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "generate-payload", Short: "Generates a payload to create/update SKE clusters", @@ -55,9 +56,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Generate a payload with values of a cluster, and preview it in the terminal`, `$ stackit ske cluster generate-payload --cluster-name my-cluster`), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -70,7 +71,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { var payload *ske.CreateOrUpdateClusterPayload if model.ClusterName == nil { - payload, err = skeUtils.GetDefaultPayload(ctx, apiClient, model.Region) + payload, err = skeUtils.GetDefaultPayload(ctx, apiClient.DefaultAPI, model.Region) if err != nil { return err } @@ -81,10 +82,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("read SKE cluster: %w", err) } payload = &ske.CreateOrUpdateClusterPayload{ + Access: resp.Access, Extensions: resp.Extensions, Hibernation: resp.Hibernation, Kubernetes: resp.Kubernetes, Maintenance: resp.Maintenance, + Network: resp.Network, Nodepools: resp.Nodepools, Status: resp.Status, } @@ -102,7 +105,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().StringP(filePathFlag, "f", "", "If set, writes the payload to the given file. If unset, writes the payload to the standard output") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) clusterName := flags.FlagToStringPointer(p, cmd, clusterNameFlag) @@ -117,20 +120,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { FilePath: flags.FlagToStringPointer(p, cmd, filePathFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiGetClusterRequest { - req := apiClient.GetCluster(ctx, model.ProjectId, model.Region, *model.ClusterName) + req := apiClient.DefaultAPI.GetCluster(ctx, model.ProjectId, model.Region, *model.ClusterName) return req } diff --git a/internal/cmd/ske/cluster/generate-payload/generate_payload_test.go b/internal/cmd/ske/cluster/generate-payload/generate_payload_test.go index 856db970d..314fe46c0 100644 --- a/internal/cmd/ske/cluster/generate-payload/generate_payload_test.go +++ b/internal/cmd/ske/cluster/generate-payload/generate_payload_test.go @@ -4,15 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -20,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() const ( @@ -60,7 +60,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiGetClusterRequest)) ske.ApiGetClusterRequest { - request := testClient.GetCluster(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.GetCluster(testCtx, testProjectId, testRegion, testClusterName) for _, mod := range mods { mod(&request) } @@ -70,6 +70,7 @@ func fixtureRequest(mods ...func(request *ske.ApiGetClusterRequest)) ske.ApiGetC func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -133,54 +134,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - err = cmd.ValidateFlagGroups() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -206,6 +160,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -245,11 +200,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.filePath, tt.args.payload); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/cluster/hibernate/hibernate.go b/internal/cmd/ske/cluster/hibernate/hibernate.go index 4c0be63ab..b2a345175 100644 --- a/internal/cmd/ske/cluster/hibernate/hibernate.go +++ b/internal/cmd/ske/cluster/hibernate/hibernate.go @@ -4,8 +4,12 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" ) const ( @@ -27,7 +29,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("hibernate %s", clusterNameArg), Short: "Trigger hibernate for a SKE cluster", @@ -56,12 +58,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to trigger hibernate for %q in project %q?", model.ClusterName, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to trigger hibernate for %q in project %q?", model.ClusterName, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -73,13 +73,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Hibernating cluster") - _, err = wait.TriggerClusterHibernationWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Hibernating cluster", func() error { + _, err = wait.TriggerClusterHibernationWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster hibernation: %w", err) } - s.Stop() } operationState := "Hibernated" @@ -106,19 +106,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiTriggerHibernateRequest { - req := apiClient.TriggerHibernate(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.TriggerHibernate(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/cluster/hibernate/hibernate_test.go b/internal/cmd/ske/cluster/hibernate/hibernate_test.go index d9d531ef1..cc19c48f4 100644 --- a/internal/cmd/ske/cluster/hibernate/hibernate_test.go +++ b/internal/cmd/ske/cluster/hibernate/hibernate_test.go @@ -7,11 +7,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) type testCtxKey struct{} @@ -22,7 +21,7 @@ const ( ) var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiTriggerHibernateRequest)) ske.ApiTriggerHibernateRequest { - request := testClient.TriggerHibernate(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.TriggerHibernate(testCtx, testProjectId, testRegion, testClusterName) for _, mod := range mods { mod(&request) } @@ -112,8 +111,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := &cobra.Command{} + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -130,14 +129,14 @@ func TestParseInput(t *testing.T) { } if len(tt.argValues) == 0 { - _, err := parseInput(p, cmd, tt.argValues) + _, err := parseInput(params.Printer, cmd, tt.argValues) if err == nil && !tt.isValid { t.Fatalf("expected error due to missing args") } return } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -177,6 +176,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmpopts.EquateComparable(testCtx), cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("request mismatch:\n%s", diff) diff --git a/internal/cmd/ske/cluster/list/list.go b/internal/cmd/ske/cluster/list/list.go index 31b8f2b7b..c3943a914 100644 --- a/internal/cmd/ske/cluster/list/list.go +++ b/internal/cmd/ske/cluster/list/list.go @@ -2,11 +2,10 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,7 +20,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) const ( @@ -33,7 +32,7 @@ type inputModel struct { Limit *int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all SKE clusters", @@ -50,9 +49,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List up to 10 SKE clusters`, "$ stackit ske cluster list --limit 10"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -69,38 +68,38 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - // Check if SKE is enabled for this project - enabled, err := serviceEnablementUtils.ProjectEnabled(ctx, serviceEnablementApiClient, model.ProjectId, model.Region) - if err != nil { - return err - } - if !enabled { - return fmt.Errorf("SKE isn't enabled for this project, please run 'stackit ske enable'") - } - // Call API req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() if err != nil { - return fmt.Errorf("get SKE clusters: %w", err) - } - clusters := *resp.Items - if len(clusters) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId + // Check if SKE is enabled for this project + enabled, enabledErr := serviceEnablementUtils.ProjectEnabled(ctx, serviceEnablementApiClient.DefaultAPI, model.ProjectId, model.Region) + if enabledErr != nil { + return fmt.Errorf("check if project is enabled failed: %w", enabledErr) + } + if !enabled { + return &errors.ServiceDisabledError{ + Service: "ske", + } } - params.Printer.Info("No clusters found for project %q\n", projectLabel) - return nil + return fmt.Errorf("get SKE clusters: %w", err) } + clusters := resp.Items // Truncate output if model.Limit != nil && len(clusters) > int(*model.Limit) { clusters = clusters[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, clusters) + projectLabel := model.ProjectId + if len(clusters) == 0 { + projectLabel, err = projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + } + } + + return outputResult(params.Printer, model.OutputFormat, projectLabel, clusters) }, } @@ -112,7 +111,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -131,60 +130,38 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiListClustersRequest { - req := apiClient.ListClusters(ctx, model.ProjectId, model.Region) + req := apiClient.DefaultAPI.ListClusters(ctx, model.ProjectId, model.Region) return req } -func outputResult(p *print.Printer, outputFormat string, clusters []ske.Cluster) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(clusters, "", " ") - if err != nil { - return fmt.Errorf("marshal SKE cluster list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, clusters []ske.Cluster) error { + return p.OutputResult(outputFormat, clusters, func() error { + if len(clusters) == 0 { + p.Outputf("No clusters found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(clusters, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SKE cluster list: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("NAME", "STATE", "VERSION", "POOLS", "MONITORING") for i := range clusters { c := clusters[i] monitoring := "Disabled" - if c.Extensions != nil && c.Extensions.Observability != nil && *c.Extensions.Observability.Enabled { + if c.Extensions != nil && c.Extensions.Observability != nil && c.Extensions.Observability.Enabled { monitoring = "Enabled" } - statusAggregated, kubernetesVersion := "", "" + statusAggregated := "" if c.HasStatus() { statusAggregated = utils.PtrString(c.Status.Aggregated) } - if c.Kubernetes != nil { - kubernetesVersion = utils.PtrString(c.Kubernetes.Version) - } + kubernetesVersion := c.Kubernetes.Version countNodepools := 0 if c.Nodepools != nil { - countNodepools = len(*c.Nodepools) + countNodepools = len(c.Nodepools) } table.AddRow( utils.PtrString(c.Name), @@ -200,5 +177,5 @@ func outputResult(p *print.Printer, outputFormat string, clusters []ske.Cluster) } return nil - } + }) } diff --git a/internal/cmd/ske/cluster/list/list_test.go b/internal/cmd/ske/cluster/list/list_test.go index e3c1a063c..d59bb6fd1 100644 --- a/internal/cmd/ske/cluster/list/list_test.go +++ b/internal/cmd/ske/cluster/list/list_test.go @@ -4,16 +4,15 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -21,7 +20,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() const testRegion = "eu01" @@ -54,7 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiListClustersRequest)) ske.ApiListClustersRequest { - request := testClient.ListClusters(testCtx, testProjectId, testRegion) + request := testClient.DefaultAPI.ListClusters(testCtx, testProjectId, testRegion) for _, mod := range mods { mod(&request) } @@ -64,6 +63,7 @@ func fixtureRequest(mods ...func(request *ske.ApiListClustersRequest)) ske.ApiLi func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -118,48 +118,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -184,6 +143,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -222,11 +182,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.clusters); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, "dummy-projectlabel", tt.args.clusters); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/cluster/maintenance/maintenance.go b/internal/cmd/ske/cluster/maintenance/maintenance.go index 3396ad7c3..fbf0f16f4 100644 --- a/internal/cmd/ske/cluster/maintenance/maintenance.go +++ b/internal/cmd/ske/cluster/maintenance/maintenance.go @@ -4,8 +4,12 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" ) const ( @@ -27,7 +29,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("maintenance %s", clusterNameArg), Short: "Trigger maintenance for a SKE cluster", @@ -56,12 +58,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to trigger maintenance for %q in project %q?", model.ClusterName, projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to trigger maintenance for %q in project %q?", model.ClusterName, projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -73,13 +73,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Performing cluster maintenance") - _, err = wait.TriggerClusterMaintenanceWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Performing cluster maintenance", func() error { + _, err = wait.TriggerClusterMaintenanceWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster maintenance to complete: %w", err) } - s.Stop() } operationState := "Performed maintenance for" @@ -106,19 +106,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiTriggerMaintenanceRequest { - req := apiClient.TriggerMaintenance(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.TriggerMaintenance(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/cluster/maintenance/maintenance_test.go b/internal/cmd/ske/cluster/maintenance/maintenance_test.go index fe0ab07cb..940bf742f 100644 --- a/internal/cmd/ske/cluster/maintenance/maintenance_test.go +++ b/internal/cmd/ske/cluster/maintenance/maintenance_test.go @@ -7,11 +7,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) type testCtxKey struct{} @@ -22,7 +21,7 @@ const ( ) var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func([]string)) []string { @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(*inputModel)) *inputModel { } func fixtureRequest(mods ...func(*ske.ApiTriggerMaintenanceRequest)) ske.ApiTriggerMaintenanceRequest { - request := testClient.TriggerMaintenance(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.TriggerMaintenance(testCtx, testProjectId, testRegion, testClusterName) for _, m := range mods { m(&request) } @@ -112,8 +111,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := &cobra.Command{} + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -130,14 +129,14 @@ func TestParseInput(t *testing.T) { } if len(tt.argValues) == 0 { - _, err := parseInput(p, cmd, tt.argValues) + _, err := parseInput(params.Printer, cmd, tt.argValues) if err == nil && !tt.isValid { t.Fatalf("expected error due to missing args") } return } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -178,6 +177,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(got, want, cmpopts.EquateComparable(testCtx), cmp.AllowUnexported(want), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("request mismatch:\n%s", diff) diff --git a/internal/cmd/ske/cluster/reconcile/reconcile.go b/internal/cmd/ske/cluster/reconcile/reconcile.go index 54c98dae0..8e499c418 100644 --- a/internal/cmd/ske/cluster/reconcile/reconcile.go +++ b/internal/cmd/ske/cluster/reconcile/reconcile.go @@ -4,8 +4,12 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -13,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" ) const ( @@ -26,7 +28,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("reconcile %s", clusterNameArg), Short: "Trigger reconcile for a SKE cluster", @@ -59,13 +61,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Performing cluster reconciliation") - _, err = wait.TriggerClusterReconciliationWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Performing cluster reconciliation", func() error { + _, err = wait.TriggerClusterReconciliationWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster reconciliation: %w", err) } - s.Stop() } operationState := "Performed reconciliation for" @@ -92,19 +94,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiTriggerReconcileRequest { - req := apiClient.TriggerReconcile(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.TriggerReconcile(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/cluster/reconcile/reconcile_test.go b/internal/cmd/ske/cluster/reconcile/reconcile_test.go index 5c96f295b..51c75e795 100644 --- a/internal/cmd/ske/cluster/reconcile/reconcile_test.go +++ b/internal/cmd/ske/cluster/reconcile/reconcile_test.go @@ -7,11 +7,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) type testCtxKey struct{} @@ -22,7 +21,7 @@ const ( ) var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func([]string)) []string { @@ -61,8 +60,8 @@ func fixtureInputModel(mods ...func(*inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *ske.ApiTriggerReconcileRequest)) ske.ApiTriggerHibernateRequest { - request := testClient.TriggerReconcile(testCtx, testProjectId, testRegion, testClusterName) +func fixtureRequest(mods ...func(request *ske.ApiTriggerReconcileRequest)) ske.ApiTriggerReconcileRequest { + request := testClient.DefaultAPI.TriggerReconcile(testCtx, testProjectId, testRegion, testClusterName) for _, m := range mods { m(&request) } @@ -112,8 +111,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := &cobra.Command{} + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -130,14 +129,14 @@ func TestParseInput(t *testing.T) { } if len(tt.argValues) == 0 { - _, err := parseInput(p, cmd, tt.argValues) + _, err := parseInput(params.Printer, cmd, tt.argValues) if err == nil && !tt.isValid { t.Fatalf("expected error due to missing args") } return } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -161,7 +160,7 @@ func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest ske.ApiTriggerHibernateRequest + expectedRequest ske.ApiTriggerReconcileRequest }{ { description: "base", @@ -178,6 +177,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(got, want, cmpopts.EquateComparable(testCtx), cmp.AllowUnexported(want), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("request mismatch:\n%s", diff) diff --git a/internal/cmd/ske/cluster/update/update.go b/internal/cmd/ske/cluster/update/update.go index 8c32f8d95..bd1facd7a 100644 --- a/internal/cmd/ske/cluster/update/update.go +++ b/internal/cmd/ske/cluster/update/update.go @@ -5,8 +5,8 @@ import ( "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" ) const ( @@ -34,7 +34,7 @@ type inputModel struct { Payload ske.CreateOrUpdateClusterPayload } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", clusterNameArg), Short: "Updates a SKE cluster", @@ -70,16 +70,14 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update cluster %q?", model.ClusterName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update cluster %q?", model.ClusterName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Check if cluster exists - exists, err := skeUtils.ClusterExists(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName) + exists, err := skeUtils.ClusterExists(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName) if err != nil { return err } @@ -97,13 +95,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Updating cluster") - _, err = wait.CreateOrUpdateClusterWaitHandler(ctx, apiClient, model.ProjectId, model.Region, name).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Updating cluster", func() error { + _, err = wait.CreateOrUpdateClusterWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, name).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster update: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, model.ClusterName, resp) @@ -141,20 +139,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Payload: payload, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiCreateOrUpdateClusterRequest { - req := apiClient.CreateOrUpdateCluster(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.CreateOrUpdateCluster(ctx, model.ProjectId, model.Region, model.ClusterName) req = req.CreateOrUpdateClusterPayload(model.Payload) return req @@ -165,29 +155,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, clusterName return fmt.Errorf("cluster is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(cluster, "", " ") - if err != nil { - return fmt.Errorf("marshal SKE cluster: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(cluster, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SKE cluster: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, cluster, func() error { operationState := "Updated" if async { operationState = "Triggered update of" } p.Info("%s cluster %q\n", operationState, clusterName) return nil - } + }) } diff --git a/internal/cmd/ske/cluster/update/update_test.go b/internal/cmd/ske/cluster/update/update_test.go index dcf82f207..7dcc0e464 100644 --- a/internal/cmd/ske/cluster/update/update_test.go +++ b/internal/cmd/ske/cluster/update/update_test.go @@ -2,19 +2,18 @@ package update import ( "context" - "fmt" "testing" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -22,53 +21,67 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" const testRegion = "eu01" var testPayload = ske.CreateOrUpdateClusterPayload{ - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("1.25.15"), + Kubernetes: ske.Kubernetes{ + Version: "1.25.15", + AdditionalProperties: map[string]any{}, }, - Nodepools: &[]ske.Nodepool{ + Nodepools: []ske.Nodepool{ { - Name: utils.Ptr("np-name"), - Machine: &ske.Machine{ - Image: &ske.Image{ - Name: utils.Ptr("flatcar"), - Version: utils.Ptr("3760.2.1"), + Name: "np-name", + Machine: ske.Machine{ + Image: ske.Image{ + Name: "flatcar", + Version: "3760.2.1", + AdditionalProperties: map[string]any{}, }, - Type: utils.Ptr("b1.2"), + Type: "b1.2", + AdditionalProperties: map[string]any{}, }, - Minimum: utils.Ptr(int64(1)), - Maximum: utils.Ptr(int64(2)), - MaxSurge: utils.Ptr(int64(1)), - Volume: &ske.Volume{ - Type: utils.Ptr("storage_premium_perf0"), - Size: utils.Ptr(int64(40)), + Minimum: int32(1), + Maximum: int32(2), + MaxSurge: utils.Ptr(int32(1)), + Volume: ske.Volume{ + Type: utils.Ptr("storage_premium_perf0"), + Size: int32(40), + AdditionalProperties: map[string]any{}, }, - AvailabilityZones: &[]string{"eu01-3"}, - Cri: &ske.CRI{Name: ske.CRINAME_DOCKER.Ptr()}, + AvailabilityZones: []string{"eu01-3"}, + Cri: &ske.CRI{ + Name: utils.Ptr("containerd"), + AdditionalProperties: map[string]any{}, + }, + AdditionalProperties: map[string]any{}, }, }, Extensions: &ske.Extension{ Acl: &ske.ACL{ - Enabled: utils.Ptr(true), - AllowedCidrs: &[]string{"0.0.0.0/0"}, + Enabled: true, + AllowedCidrs: []string{"0.0.0.0/0"}, + AdditionalProperties: map[string]any{}, }, + AdditionalProperties: map[string]any{}, }, Maintenance: &ske.Maintenance{ - AutoUpdate: &ske.MaintenanceAutoUpdate{ - KubernetesVersion: utils.Ptr(true), - MachineImageVersion: utils.Ptr(true), + AutoUpdate: ske.MaintenanceAutoUpdate{ + KubernetesVersion: utils.Ptr(true), + MachineImageVersion: utils.Ptr(true), + AdditionalProperties: map[string]any{}, }, - TimeWindow: &ske.TimeWindow{ - End: utils.Ptr(time.Date(0, 1, 1, 5, 0, 0, 0, time.FixedZone("test-zone", 2*60*60))), - Start: utils.Ptr(time.Date(0, 1, 1, 3, 0, 0, 0, time.FixedZone("test-zone", 2*60*60))), + TimeWindow: ske.TimeWindow{ + End: time.Date(0, 1, 1, 5, 0, 0, 0, time.FixedZone("test-zone", 2*60*60)), + Start: time.Date(0, 1, 1, 3, 0, 0, 0, time.FixedZone("test-zone", 2*60*60)), + AdditionalProperties: map[string]any{}, }, + AdditionalProperties: map[string]any{}, }, + AdditionalProperties: map[string]any{}, } func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -85,8 +98,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, globalflags.RegionFlag: testRegion, - payloadFlag: fmt.Sprintf(`{ - "name": "cli-jp", + payloadFlag: `{ "kubernetes": { "version": "1.25.15" }, @@ -104,7 +116,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st "maximum": 2, "maxSurge": 1, "volume": { "type": "storage_premium_perf0", "size": 40 }, - "cri": { "name": "%s" }, + "cri": { "name": "containerd" }, "availabilityZones": ["eu01-3"] } ], @@ -119,7 +131,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st "start": "0000-01-01T03:00:00+02:00" } } - }`, ske.CRINAME_DOCKER), + }`, } for _, mod := range mods { mod(flagValues) @@ -144,7 +156,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiCreateOrUpdateClusterRequest)) ske.ApiCreateOrUpdateClusterRequest { - request := testClient.CreateOrUpdateCluster(testCtx, testProjectId, testRegion, fixtureInputModel().ClusterName) + request := testClient.DefaultAPI.CreateOrUpdateCluster(testCtx, testProjectId, testRegion, fixtureInputModel().ClusterName) request = request.CreateOrUpdateClusterPayload(testPayload) for _, mod := range mods { mod(&request) @@ -221,54 +233,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -294,6 +259,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -327,11 +293,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.clusterName, tt.args.cluster); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.clusterName, tt.args.cluster); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/cluster/wakeup/wakeup.go b/internal/cmd/ske/cluster/wakeup/wakeup.go index e7b9b6534..2ed99d8ad 100644 --- a/internal/cmd/ske/cluster/wakeup/wakeup.go +++ b/internal/cmd/ske/cluster/wakeup/wakeup.go @@ -4,8 +4,12 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -13,8 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" ) const ( @@ -26,7 +28,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("wakeup %s", clusterNameArg), Short: "Trigger wakeup from hibernation for a SKE cluster", @@ -59,13 +61,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Performing cluster wakeup") - _, err = wait.TriggerClusterWakeupWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Performing cluster wakeup", func() error { + _, err = wait.TriggerClusterWakeupWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE cluster wakeup: %w", err) } - s.Stop() } operationState := "Performed wakeup of" @@ -92,19 +94,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiTriggerWakeupRequest { - req := apiClient.TriggerWakeup(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.TriggerWakeup(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/cluster/wakeup/wakeup_test.go b/internal/cmd/ske/cluster/wakeup/wakeup_test.go index dd93881c1..85b2b7823 100644 --- a/internal/cmd/ske/cluster/wakeup/wakeup_test.go +++ b/internal/cmd/ske/cluster/wakeup/wakeup_test.go @@ -7,11 +7,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) type testCtxKey struct{} @@ -22,7 +21,7 @@ const ( ) var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() func fixtureArgValues(mods ...func([]string)) []string { @@ -60,7 +59,7 @@ func fixtureInputModel(mods ...func(*inputModel)) *inputModel { } func fixtureRequest(mods ...func(*ske.ApiTriggerWakeupRequest)) ske.ApiTriggerWakeupRequest { - req := testClient.TriggerWakeup(testCtx, testProjectId, testRegion, testClusterName) + req := testClient.DefaultAPI.TriggerWakeup(testCtx, testProjectId, testRegion, testClusterName) for _, m := range mods { m(&req) } @@ -110,8 +109,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := &cobra.Command{} + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -128,14 +127,14 @@ func TestParseInput(t *testing.T) { } if len(tt.argValues) == 0 { - _, err := parseInput(p, cmd, tt.argValues) + _, err := parseInput(params.Printer, cmd, tt.argValues) if err == nil && !tt.isValid { t.Fatalf("expected failure due to missing args") } return } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -159,7 +158,7 @@ func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest ske.ApiTriggerHibernateRequest + expectedRequest ske.ApiTriggerWakeupRequest }{ { description: "base", @@ -176,6 +175,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(got, want, cmpopts.EquateComparable(testCtx), cmp.AllowUnexported(want), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("request mismatch:\n%s", diff) diff --git a/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go b/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go index 982e9267d..734a89735 100644 --- a/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go +++ b/internal/cmd/ske/credentials/complete-rotation/complete_rotation.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("complete-rotation %s", clusterNameArg), Short: "Completes the rotation of the credentials associated to a SKE cluster", @@ -40,7 +41,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { " $ stackit ske kubeconfig create my-cluster", "If you haven't, please start the process by running:", " $ stackit ske credentials start-rotation my-cluster", - "For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html", + "For more information, visit: https://docs.stackit.cloud/products/runtime/kubernetes-engine/how-tos/rotate-ske-credentials/", ), Args: args.SingleArg(clusterNameArg, nil), Example: examples.Build( @@ -68,12 +69,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to complete the rotation of the credentials for SKE cluster %q?", model.ClusterName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to complete the rotation of the credentials for SKE cluster %q?", model.ClusterName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -85,13 +84,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Completing credentials rotation") - _, err = wait.CompleteCredentialsRotationWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Completing credentials rotation", func() error { + _, err = wait.CompleteCredentialsRotationWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for completing SKE credentials rotation %w", err) } - s.Stop() } operationState := "Rotation of credentials is completed" @@ -119,19 +118,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiCompleteCredentialsRotationRequest { - req := apiClient.CompleteCredentialsRotation(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.CompleteCredentialsRotation(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go b/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go index b8d40624c..9ef9dc2f8 100644 --- a/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go +++ b/internal/cmd/ske/credentials/complete-rotation/complete_rotation_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiCompleteCredentialsRotationRequest)) ske.ApiCompleteCredentialsRotationRequest { - request := testClient.CompleteCredentialsRotation(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.CompleteCredentialsRotation(testCtx, testProjectId, testRegion, testClusterName) for _, mod := range mods { mod(&request) } @@ -130,54 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,6 +154,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/ske/credentials/credentials.go b/internal/cmd/ske/credentials/credentials.go index 2770f349a..13218d2fa 100644 --- a/internal/cmd/ske/credentials/credentials.go +++ b/internal/cmd/ske/credentials/credentials.go @@ -1,16 +1,16 @@ package credentials import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" completerotation "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/complete-rotation" startrotation "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials/start-rotation" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "credentials", Short: "Provides functionality for SKE credentials", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(startrotation.NewCmd(params)) cmd.AddCommand(completerotation.NewCmd(params)) } diff --git a/internal/cmd/ske/credentials/start-rotation/start_rotation.go b/internal/cmd/ske/credentials/start-rotation/start_rotation.go index a8314ad83..ea2933fab 100644 --- a/internal/cmd/ske/credentials/start-rotation/start_rotation.go +++ b/internal/cmd/ske/credentials/start-rotation/start_rotation.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +15,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" - "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api/wait" ) const ( @@ -27,7 +28,7 @@ type inputModel struct { ClusterName string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("start-rotation %s", clusterNameArg), Short: "Starts the rotation of the credentials associated to a SKE cluster", @@ -44,7 +45,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { " $ stackit ske kubeconfig create my-cluster", "Complete the rotation by running:", " $ stackit ske credentials complete-rotation my-cluster", - "For more information, visit: https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html", + "For more information, visit: https://docs.stackit.cloud/products/runtime/kubernetes-engine/how-tos/rotate-ske-credentials/", ), Args: args.SingleArg(clusterNameArg, nil), Example: examples.Build( @@ -71,12 +72,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to start the rotation of the credentials for SKE cluster %q?", model.ClusterName) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to start the rotation of the credentials for SKE cluster %q?", model.ClusterName) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -88,13 +87,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Starting credentials rotation") - _, err = wait.StartCredentialsRotationWaitHandler(ctx, apiClient, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Starting credentials rotation", func() error { + _, err = wait.StartCredentialsRotationWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.ClusterName).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for start SKE credentials rotation %w", err) } - s.Stop() } operationState := "Rotation of credentials is ready to be completed" @@ -122,19 +121,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) ske.ApiStartCredentialsRotationRequest { - req := apiClient.StartCredentialsRotation(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.StartCredentialsRotation(ctx, model.ProjectId, model.Region, model.ClusterName) return req } diff --git a/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go b/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go index 0e2f99fa3..e3fae33d2 100644 --- a/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go +++ b/internal/cmd/ske/credentials/start-rotation/start_rotation_test.go @@ -4,14 +4,13 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +18,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" @@ -62,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiStartCredentialsRotationRequest)) ske.ApiStartCredentialsRotationRequest { - request := testClient.StartCredentialsRotation(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.StartCredentialsRotation(testCtx, testProjectId, testRegion, testClusterName) for _, mod := range mods { mod(&request) } @@ -130,54 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,6 +154,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/ske/describe/describe.go b/internal/cmd/ske/describe/describe.go index da717570e..68804458b 100644 --- a/internal/cmd/ske/describe/describe.go +++ b/internal/cmd/ske/describe/describe.go @@ -2,12 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,14 +18,13 @@ import ( skeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "describe", Short: "Shows overall details regarding SKE", @@ -35,9 +35,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Get details regarding SKE functionality on your project`, "$ stackit ske describe"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -60,7 +60,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -70,20 +70,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serviceenablement.APIClient) serviceenablement.ApiGetServiceStatusRegionalRequest { - req := apiClient.GetServiceStatusRegional(ctx, model.Region, model.ProjectId, skeUtils.SKEServiceId) + req := apiClient.DefaultAPI.GetServiceStatusRegional(ctx, model.Region, model.ProjectId, skeUtils.SKEServiceId) return req } @@ -92,24 +84,7 @@ func outputResult(p *print.Printer, outputFormat string, project *serviceenablem return fmt.Errorf("project is nil") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(project, "", " ") - if err != nil { - return fmt.Errorf("marshal SKE project details: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(project, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SKE project details: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, project, func() error { table := tables.NewTable() table.AddRow("ID", projectId) table.AddSeparator() @@ -122,5 +97,5 @@ func outputResult(p *print.Printer, outputFormat string, project *serviceenablem } return nil - } + }) } diff --git a/internal/cmd/ske/describe/describe_test.go b/internal/cmd/ske/describe/describe_test.go index 4fb7cbb64..4104ef5f0 100644 --- a/internal/cmd/ske/describe/describe_test.go +++ b/internal/cmd/ske/describe/describe_test.go @@ -4,24 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" serviceEnablementUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serviceenablement.APIClient{} +var testClient = &serviceenablement.APIClient{DefaultAPI: &serviceenablement.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -49,7 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serviceenablement.ApiGetServiceStatusRegionalRequest)) serviceenablement.ApiGetServiceStatusRegionalRequest { - request := testClient.GetServiceStatusRegional(testCtx, testRegion, testProjectId, serviceEnablementUtils.SKEServiceId) //nolint:staticcheck //command will be removed in a later update + request := testClient.DefaultAPI.GetServiceStatusRegional(testCtx, testRegion, testProjectId, serviceEnablementUtils.SKEServiceId) //nolint:staticcheck //command will be removed in a later update for _, mod := range mods { mod(&request) } @@ -59,6 +60,7 @@ func fixtureRequest(mods ...func(request *serviceenablement.ApiGetServiceStatusR func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -99,46 +101,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -163,7 +126,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serviceenablement.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -196,11 +159,11 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.project, tt.args.projectId); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.project, tt.args.projectId); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/disable/disable.go b/internal/cmd/ske/disable/disable.go index ea355ada1..f2607018d 100644 --- a/internal/cmd/ske/disable/disable.go +++ b/internal/cmd/ske/disable/disable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,15 +17,15 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/wait" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api/wait" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "disable", Short: "Disables SKE for a project", @@ -35,9 +36,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Disable SKE functionality for your project, deleting all associated clusters`, "$ stackit ske disable"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -54,12 +55,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to disable SKE for project %q? (This will delete all associated clusters)", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to disable SKE for project %q? (This will delete all associated clusters)", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -71,13 +70,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Disabling SKE") - _, err = wait.DisableServiceWaitHandler(ctx, apiClient, model.Region, model.ProjectId, utils.SKEServiceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Disabling SKE", func() error { + _, err = wait.DisableServiceWaitHandler(ctx, apiClient.DefaultAPI, model.Region, model.ProjectId, utils.SKEServiceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE disabling: %w", err) } - s.Stop() } operationState := "Disabled" @@ -91,7 +90,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -101,19 +100,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serviceenablement.APIClient) serviceenablement.ApiDisableServiceRegionalRequest { - req := apiClient.DisableServiceRegional(ctx, model.Region, model.ProjectId, utils.SKEServiceId) + req := apiClient.DefaultAPI.DisableServiceRegional(ctx, model.Region, model.ProjectId, utils.SKEServiceId) return req } diff --git a/internal/cmd/ske/disable/disable_test.go b/internal/cmd/ske/disable/disable_test.go index b377dc366..cf2987742 100644 --- a/internal/cmd/ske/disable/disable_test.go +++ b/internal/cmd/ske/disable/disable_test.go @@ -5,22 +5,22 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serviceenablement.APIClient{} +var testClient = &serviceenablement.APIClient{DefaultAPI: &serviceenablement.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -48,7 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serviceenablement.ApiDisableServiceRegionalRequest)) serviceenablement.ApiDisableServiceRegionalRequest { - request := testClient.DisableServiceRegional(testCtx, testRegion, testProjectId, utils.SKEServiceId) + request := testClient.DefaultAPI.DisableServiceRegional(testCtx, testRegion, testProjectId, utils.SKEServiceId) for _, mod := range mods { mod(&request) } @@ -58,6 +58,7 @@ func fixtureRequest(mods ...func(request *serviceenablement.ApiDisableServiceReg func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -98,46 +99,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -161,7 +123,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serviceenablement.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/ske/enable/enable.go b/internal/cmd/ske/enable/enable.go index 91431b5e3..6fed3949a 100644 --- a/internal/cmd/ske/enable/enable.go +++ b/internal/cmd/ske/enable/enable.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,15 +17,15 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/wait" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api/wait" ) type inputModel struct { *globalflags.GlobalFlagModel } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "enable", Short: "Enables SKE for a project", @@ -35,9 +36,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Enable SKE functionality for your project`, "$ stackit ske enable"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -54,12 +55,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to enable SKE for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to enable SKE for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -71,13 +70,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Enabling SKE") - _, err = wait.EnableServiceWaitHandler(ctx, apiClient, model.Region, model.ProjectId, utils.SKEServiceId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Enabling SKE", func() error { + _, err = wait.EnableServiceWaitHandler(ctx, apiClient.DefaultAPI, model.Region, model.ProjectId, utils.SKEServiceId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for SKE enabling: %w", err) } - s.Stop() } operationState := "Enabled" @@ -91,7 +90,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -101,19 +100,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { GlobalFlagModel: globalFlags, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *serviceenablement.APIClient) serviceenablement.ApiEnableServiceRegionalRequest { - req := apiClient.EnableServiceRegional(ctx, model.Region, model.ProjectId, utils.SKEServiceId) + req := apiClient.DefaultAPI.EnableServiceRegional(ctx, model.Region, model.ProjectId, utils.SKEServiceId) return req } diff --git a/internal/cmd/ske/enable/enable_test.go b/internal/cmd/ske/enable/enable_test.go index d46ec2d07..1ecea575a 100644 --- a/internal/cmd/ske/enable/enable_test.go +++ b/internal/cmd/ske/enable/enable_test.go @@ -5,22 +5,22 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/service-enablement/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &serviceenablement.APIClient{} +var testClient = &serviceenablement.APIClient{DefaultAPI: &serviceenablement.DefaultAPIService{}} var testProjectId = uuid.NewString() -var testRegion = "eu01" + +const testRegion = "eu01" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ @@ -48,7 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *serviceenablement.ApiEnableServiceRegionalRequest)) serviceenablement.ApiEnableServiceRegionalRequest { - request := testClient.EnableServiceRegional(testCtx, testRegion, testProjectId, utils.SKEServiceId) + request := testClient.DefaultAPI.EnableServiceRegional(testCtx, testRegion, testProjectId, utils.SKEServiceId) for _, mod := range mods { mod(&request) } @@ -58,6 +58,7 @@ func fixtureRequest(mods ...func(request *serviceenablement.ApiEnableServiceRegi func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -98,46 +99,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -161,7 +123,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, serviceenablement.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/ske/kubeconfig/create/create.go b/internal/cmd/ske/kubeconfig/create/create.go index f420009f0..96093f901 100644 --- a/internal/cmd/ske/kubeconfig/create/create.go +++ b/internal/cmd/ske/kubeconfig/create/create.go @@ -5,8 +5,10 @@ import ( "encoding/json" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +20,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) const ( @@ -28,6 +30,7 @@ const ( expirationFlag = "expiration" filepathFlag = "filepath" loginFlag = "login" + idpFlag = "idp" overwriteFlag = "overwrite" ) @@ -38,42 +41,47 @@ type inputModel struct { ExpirationTime *string Filepath *string Login bool + IDP bool Overwrite bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("create %s", clusterNameArg), Short: "Creates or update a kubeconfig for a SKE cluster", Long: fmt.Sprintf("%s\n\n%s\n%s\n%s\n%s", - "Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster, if the config exists in the kubeconfig file the information will be updated.", - "By default, the kubeconfig information of the SKE cluster is merged into the default kubeconfig file of the current user. If the kubeconfig file doesn't exist, a new one will be created.", + "Creates a kubeconfig for a STACKIT Kubernetes Engine (SKE) cluster. By default an admin kubeconfig is created. Use the `--idp` option to create an IDP kubeconfig that authenticates via the STACKIT IDP.", + "If the config exists in the kubeconfig file the information will be updated. By default, the kubeconfig information of the SKE cluster is merged into the default kubeconfig file of the current user. If the kubeconfig file doesn't exist, a new one will be created.", "You can override this behavior by specifying a custom filepath using the --filepath flag or by setting the KUBECONFIG env variable (fallback).\n", "An expiration time can be set for the kubeconfig. The expiration time is set in seconds(s), minutes(m), hours(h), days(d) or months(M). Default is 1h.\n", "Note that the format is , e.g. 30d for 30 days and you can't combine units."), Args: args.SingleArg(clusterNameArg, nil), Example: examples.Build( examples.NewExample( - `Create or update a kubeconfig for the SKE cluster with name "my-cluster. If the config exits in the kubeconfig file the information will be updated."`, - "$ stackit ske kubeconfig create my-cluster"), - examples.NewExample( - `Get a login kubeconfig for the SKE cluster with name "my-cluster". `+ - "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", + `Get a short-lived admin kubeconfig for the SKE cluster with name "my-cluster". `+ + "This kubeconfig does not contain any credentials and instead obtains valid admin credentials via the `stackit ske kubeconfig login` command.", "$ stackit ske kubeconfig create my-cluster --login"), examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated.`, + `Get an IDP kubeconfig for the SKE cluster with name "my-cluster". `+ + "This kubeconfig does not grant permissions in the cluster by default and obtains credentials on-demand via the `stackit ske kubeconfig login` command.", + "$ stackit ske kubeconfig create my-cluster --idp"), + examples.NewExample( + `Create or update a short-lived admin kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath. If the config exits in the kubeconfig file the information will be updated.`, + "$ stackit ske kubeconfig create my-cluster --login --filepath /path/to/config"), + examples.NewExample( + `Create or update an admin kubeconfig for the SKE cluster with name "my-cluster". If the config exits in the kubeconfig file the information will be updated."`, + "$ stackit ske kubeconfig create my-cluster"), + examples.NewExample( + `Create an admin kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days. If the config exits in the kubeconfig file the information will be updated.`, "$ stackit ske kubeconfig create my-cluster --expiration 30d"), examples.NewExample( - `Create or update a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated.`, + `Create or update an admin kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 2 months. If the config exits in the kubeconfig file the information will be updated.`, "$ stackit ske kubeconfig create my-cluster --expiration 2M"), examples.NewExample( - `Create or update a kubeconfig for the SKE cluster with name "my-cluster" in a custom filepath. If the config exits in the kubeconfig file the information will be updated.`, - "$ stackit ske kubeconfig create my-cluster --filepath /path/to/config"), - examples.NewExample( - `Get a kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json`, + `Get an admin kubeconfig for the SKE cluster with name "my-cluster" without writing it to a file and format the output as json`, "$ stackit ske kubeconfig create my-cluster --disable-writing --output-format json"), examples.NewExample( - `Create a kubeconfig for the SKE cluster with name "my-cluster. It will OVERWRITE your current kubeconfig file."`, + `Create an admin kubeconfig for the SKE cluster with name "my-cluster". It will OVERWRITE your current kubeconfig file.`, "$ stackit ske kubeconfig create my-cluster --overwrite true"), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -89,12 +97,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - if !model.AssumeYes && !model.DisableWriting { + if !model.DisableWriting { var prompt string if model.Overwrite { prompt = fmt.Sprintf("Are you sure you want to create a kubeconfig for SKE cluster %q? This will OVERWRITE your current kubeconfig file, if it exists.", model.ClusterName) } else { - prompt = fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \nIf it the kubeconfig file doesn't exists, it will create a new one.", model.ClusterName) + prompt = fmt.Sprintf("Are you sure you want to update your kubeconfig for SKE cluster %q? This will update your kubeconfig file. \nIf the kubeconfig file does not exists, it will create a new one.", model.ClusterName) } err = params.Printer.PromptForConfirmation(prompt) if err != nil { @@ -107,22 +115,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { kubeconfig string respKubeconfig *ske.Kubeconfig respLogin *ske.LoginKubeconfig + respIDP *ske.IDPKubeconfig ) - if !model.Login { - req, err := buildRequestCreate(ctx, model, apiClient) - if err != nil { - return fmt.Errorf("build kubeconfig create request: %w", err) - } - respKubeconfig, err = req.Execute() - if err != nil { - return fmt.Errorf("create kubeconfig for SKE cluster: %w", err) - } - if respKubeconfig.Kubeconfig == nil { - return fmt.Errorf("no kubeconfig returned from the API") - } - kubeconfig = *respKubeconfig.Kubeconfig - } else { + if model.Login { req, err := buildRequestLogin(ctx, model, apiClient) if err != nil { return fmt.Errorf("build login kubeconfig create request: %w", err) @@ -135,6 +131,32 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("no login kubeconfig returned from the API") } kubeconfig = *respLogin.Kubeconfig + } else if model.IDP { + req, err := buildRequestIDP(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("build idp kubeconfig create request: %w", err) + } + respIDP, err = req.Execute() + if err != nil { + return fmt.Errorf("create idp kubeconfig for SKE cluster: %w", err) + } + if respIDP.Kubeconfig == nil { + return fmt.Errorf("no idp kubeconfig returned from the API") + } + kubeconfig = *respIDP.Kubeconfig + } else { + req, err := buildRequestCreate(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("build kubeconfig create request: %w", err) + } + respKubeconfig, err = req.Execute() + if err != nil { + return fmt.Errorf("create kubeconfig for SKE cluster: %w", err) + } + if respKubeconfig.Kubeconfig == nil { + return fmt.Errorf("no kubeconfig returned from the API") + } + kubeconfig = *respKubeconfig.Kubeconfig } // Create the config file @@ -160,7 +182,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { params.Printer.Outputf("\nSet kubectl context to %s with: kubectl config use-context %s\n", model.ClusterName, model.ClusterName) } - return outputResult(params.Printer, model.OutputFormat, model.ClusterName, kubeconfigPath, respKubeconfig, respLogin) + return outputResult(params.Printer, model.OutputFormat, model.ClusterName, kubeconfigPath, respKubeconfig, respLogin, respIDP) }, } configureFlags(cmd) @@ -169,11 +191,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(disableWritingFlag, false, fmt.Sprintf("Disable the writing of kubeconfig. Set the output format to json or yaml using the --%s flag to display the kubeconfig.", globalflags.OutputFormatFlag)) - cmd.Flags().BoolP(loginFlag, "l", false, "Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag.") + cmd.Flags().BoolP(loginFlag, "l", false, "Create a short-lived admin kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag.") + cmd.Flags().Bool(idpFlag, false, "Create a non-admin kubeconfig that uses the STACKIT IDP to obtain credentials.") cmd.Flags().String(filepathFlag, "", "Path to create the kubeconfig file. Will fall back to KUBECONFIG env variable if not set. In case both aren't set, the kubeconfig is created as file named 'config' in the .kube folder in the user's home directory.") cmd.Flags().StringP(expirationFlag, "e", "", "Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h") cmd.Flags().Bool(overwriteFlag, false, "Overwrite the kubeconfig file.") - cmd.MarkFlagsMutuallyExclusive(loginFlag, expirationFlag) + cmd.MarkFlagsMutuallyExclusive(loginFlag, expirationFlag, idpFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -212,23 +235,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Filepath: flags.FlagToStringPointer(p, cmd, filepathFlag), GlobalFlagModel: globalFlags, Login: flags.FlagToBoolValue(p, cmd, loginFlag), + IDP: flags.FlagToBoolValue(p, cmd, idpFlag), Overwrite: flags.FlagToBoolValue(p, cmd, overwriteFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequestCreate(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiCreateKubeconfigRequest, error) { - req := apiClient.CreateKubeconfig(ctx, model.ProjectId, model.Region, model.ClusterName) + req := apiClient.DefaultAPI.CreateKubeconfig(ctx, model.ProjectId, model.Region, model.ClusterName) payload := ske.CreateKubeconfigPayload{} @@ -239,11 +255,15 @@ func buildRequestCreate(ctx context.Context, model *inputModel, apiClient *ske.A return req.CreateKubeconfigPayload(payload), nil } +func buildRequestIDP(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiGetIDPKubeconfigRequest, error) { + return apiClient.DefaultAPI.GetIDPKubeconfig(ctx, model.ProjectId, model.Region, model.ClusterName), nil +} + func buildRequestLogin(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiGetLoginKubeconfigRequest, error) { - return apiClient.GetLoginKubeconfig(ctx, model.ProjectId, model.Region, model.ClusterName), nil + return apiClient.DefaultAPI.GetLoginKubeconfig(ctx, model.ProjectId, model.Region, model.ClusterName), nil } -func outputResult(p *print.Printer, outputFormat, clusterName, kubeconfigPath string, respKubeconfig *ske.Kubeconfig, respLogin *ske.LoginKubeconfig) error { +func outputResult(p *print.Printer, outputFormat, clusterName, kubeconfigPath string, respKubeconfig *ske.Kubeconfig, respLogin *ske.LoginKubeconfig, respIDP *ske.IDPKubeconfig) error { switch outputFormat { case print.JSONOutputFormat: var err error @@ -252,6 +272,8 @@ func outputResult(p *print.Printer, outputFormat, clusterName, kubeconfigPath st details, err = json.MarshalIndent(respKubeconfig, "", " ") } else if respLogin != nil { details, err = json.MarshalIndent(respLogin, "", " ") + } else if respIDP != nil { + details, err = json.MarshalIndent(respIDP, "", " ") } if err != nil { return fmt.Errorf("marshal SKE Kubeconfig: %w", err) @@ -266,6 +288,8 @@ func outputResult(p *print.Printer, outputFormat, clusterName, kubeconfigPath st details, err = yaml.MarshalWithOptions(respKubeconfig, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) } else if respLogin != nil { details, err = yaml.MarshalWithOptions(respLogin, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + } else if respIDP != nil { + details, err = yaml.MarshalWithOptions(respIDP, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) } if err != nil { return fmt.Errorf("marshal SKE Kubeconfig: %w", err) diff --git a/internal/cmd/ske/kubeconfig/create/create_test.go b/internal/cmd/ske/kubeconfig/create/create_test.go index 513fae126..c12c06887 100644 --- a/internal/cmd/ske/kubeconfig/create/create_test.go +++ b/internal/cmd/ske/kubeconfig/create/create_test.go @@ -7,11 +7,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" ) var projectIdFlag = globalflags.ProjectIdFlag @@ -19,7 +21,7 @@ var projectIdFlag = globalflags.ProjectIdFlag type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" @@ -62,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest { - request := testClient.CreateKubeconfig(testCtx, testProjectId, testRegion, testClusterName) + request := testClient.DefaultAPI.CreateKubeconfig(testCtx, testProjectId, testRegion, testClusterName) request = request.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{}) for _, mod := range mods { mod(&request) @@ -70,6 +72,14 @@ func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.A return request } +func fixtureRequestLogin() ske.ApiGetLoginKubeconfigRequest { + return testClient.DefaultAPI.GetLoginKubeconfig(testCtx, testProjectId, testRegion, testClusterName) +} + +func fixtureRequestIDP() ske.ApiGetIDPKubeconfigRequest { + return testClient.DefaultAPI.GetIDPKubeconfig(testCtx, testProjectId, testRegion, testClusterName) +} + func TestParseInput(t *testing.T) { tests := []struct { description string @@ -107,6 +117,17 @@ func TestParseInput(t *testing.T) { model.Login = true }), }, + { + description: "idp", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues["idp"] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.IDP = true + }), + }, { description: "custom filepath", argValues: fixtureArgValues(), @@ -173,7 +194,7 @@ func TestParseInput(t *testing.T) { argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { flagValues[disableWritingFlag] = "true" - flagValues[globalflags.OutputFormatFlag] = print.YAMLOutputFormat + flagValues[globalflags.OutputFormatFlag.Name()] = print.YAMLOutputFormat }), expectedModel: fixtureInputModel(func(model *inputModel) { model.DisableWriting = true @@ -207,54 +228,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -283,18 +257,37 @@ func TestBuildRequestCreate(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { request, _ := buildRequestCreate(testCtx, tt.model, testClient) - - diff := cmp.Diff(request, tt.expectedRequest, - cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), - ) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + assertNoDiff(t, request, tt.expectedRequest) }) } } +func assertNoDiff(t *testing.T, actual, expected any) { + t.Helper() + diff := cmp.Diff(actual, expected, + cmp.AllowUnexported(expected), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } +} + +func TestBuildRequestLogin(t *testing.T) { + model := fixtureInputModel() + expectedRequest := fixtureRequestLogin() + request, _ := buildRequestLogin(testCtx, model, testClient) + assertNoDiff(t, request, expectedRequest) +} + +func TestBuildRequestIDP(t *testing.T) { + model := fixtureInputModel() + expectedRequest := fixtureRequestIDP() + request, _ := buildRequestIDP(testCtx, model, testClient) + assertNoDiff(t, request, expectedRequest) +} + func Test_outputResult(t *testing.T) { type args struct { outputFormat string @@ -302,6 +295,7 @@ func Test_outputResult(t *testing.T) { kubeconfigPath string respKubeconfig *ske.Kubeconfig respLogin *ske.LoginKubeconfig + respIDP *ske.IDPKubeconfig } tests := []struct { name string @@ -327,12 +321,19 @@ func Test_outputResult(t *testing.T) { }, wantErr: false, }, + { + name: "missing idp", + args: args{ + respIDP: &ske.IDPKubeconfig{}, + }, + wantErr: false, + }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.clusterName, tt.args.kubeconfigPath, tt.args.respKubeconfig, tt.args.respLogin); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.clusterName, tt.args.kubeconfigPath, tt.args.respKubeconfig, tt.args.respLogin, tt.args.respIDP); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/kubeconfig/kubeconfig.go b/internal/cmd/ske/kubeconfig/kubeconfig.go index ad5482dbe..e1fb827c2 100644 --- a/internal/cmd/ske/kubeconfig/kubeconfig.go +++ b/internal/cmd/ske/kubeconfig/kubeconfig.go @@ -1,16 +1,16 @@ package kubeconfig import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig/create" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig/login" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "kubeconfig", Short: "Provides functionality for SKE kubeconfig", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(login.NewCmd(params)) } diff --git a/internal/cmd/ske/kubeconfig/login/login.go b/internal/cmd/ske/kubeconfig/login/login.go index 3edc33b1f..268831202 100644 --- a/internal/cmd/ske/kubeconfig/login/login.go +++ b/internal/cmd/ske/kubeconfig/login/login.go @@ -8,49 +8,60 @@ import ( "encoding/pem" "errors" "fmt" + "net/http" "os" "strconv" "time" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/cache" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "k8s.io/client-go/rest" - - "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/examples" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/ske" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/auth/exec" "k8s.io/client-go/tools/clientcmd" + + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/cache" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" ) const ( - expirationSeconds = 30 * 60 // 30 min - refreshBeforeDuration = 15 * time.Minute // 15 min + expirationSeconds = 30 * 60 // 30 min + refreshBeforeDuration = 15 * time.Minute // 15 min + refreshTokenBeforeDuration = 5 * time.Minute // 5 min + + idpFlag = "idp" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "login", Short: "Login plugin for kubernetes clients", Long: fmt.Sprintf("%s\n%s\n%s", "Login plugin for kubernetes clients, that creates short-lived credentials to authenticate against a STACKIT Kubernetes Engine (SKE) cluster.", - "First you need to obtain a kubeconfig for use with the login command (first example).", - "Secondly you use the kubeconfig with your chosen Kubernetes client (second example), the client will automatically retrieve the credentials via the STACKIT CLI.", + "First you need to obtain a kubeconfig for use with the login command (first or second example).", + "Secondly you use the kubeconfig with your chosen Kubernetes client (third example), the client will automatically retrieve the credentials via the STACKIT CLI.", ), Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `Get a login kubeconfig for the SKE cluster with name "my-cluster". `+ - "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", + `Get an admin, login kubeconfig for the SKE cluster with name "my-cluster". `+ + "This kubeconfig does not contain any credentials and instead obtains valid admin credentials via the `stackit ske kubeconfig login` command.", "$ stackit ske kubeconfig create my-cluster --login"), + examples.NewExample( + `Get an IDP kubeconfig for the SKE cluster with name "my-cluster". `+ + "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", + "$ stackit ske kubeconfig create my-cluster --idp"), examples.NewExample( "Use the previously saved kubeconfig to authenticate to the SKE cluster, in this case with kubectl.", "$ kubectl cluster-info", @@ -70,64 +81,55 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "See `stackit ske kubeconfig login --help` for detailed usage instructions.") } - clusterConfig, err := parseClusterConfig(params.Printer, cmd) + idpMode := flags.FlagToBoolValue(params.Printer, cmd, idpFlag) + clusterConfig, err := parseClusterConfig(params.Printer, cmd, idpMode) if err != nil { return fmt.Errorf("parseClusterConfig: %w", err) } + if idpMode { + accessToken, err := getAccessToken(params) + if err != nil { + return err + } + idpClient := &http.Client{} + token, err := retrieveTokenFromIDP(ctx, idpClient, accessToken, clusterConfig) + if err != nil { + return err + } + return outputTokenKubeconfig(params.Printer, clusterConfig.cacheKey, token) + } + // Configure API client apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) if err != nil { return err } - - cachedKubeconfig := getCachedKubeConfig(clusterConfig.cacheKey) - - if cachedKubeconfig == nil { - return GetAndOutputKubeconfig(ctx, params.Printer, apiClient, clusterConfig, false, nil) - } - - certPem, _ := pem.Decode(cachedKubeconfig.CertData) - if certPem == nil { - _ = cache.DeleteObject(clusterConfig.cacheKey) - return GetAndOutputKubeconfig(ctx, params.Printer, apiClient, clusterConfig, false, nil) - } - - certificate, err := x509.ParseCertificate(certPem.Bytes) + kubeconfig, err := retrieveLoginKubeconfig(ctx, apiClient, clusterConfig) if err != nil { - _ = cache.DeleteObject(clusterConfig.cacheKey) - return GetAndOutputKubeconfig(ctx, params.Printer, apiClient, clusterConfig, false, nil) - } - - // cert is expired, request new - if time.Now().After(certificate.NotAfter.UTC()) { - _ = cache.DeleteObject(clusterConfig.cacheKey) - return GetAndOutputKubeconfig(ctx, params.Printer, apiClient, clusterConfig, false, nil) - } - // cert expires within the next 15min, refresh (try to get a new, use cache on failure) - if time.Now().Add(refreshBeforeDuration).After(certificate.NotAfter.UTC()) { - return GetAndOutputKubeconfig(ctx, params.Printer, apiClient, clusterConfig, true, cachedKubeconfig) - } - - // cert not expired, nor will it expire in the next 15min; therefore, use the cached kubeconfig - if err := output(params.Printer, clusterConfig.cacheKey, cachedKubeconfig); err != nil { return err } - return nil + return outputLoginKubeconfig(params.Printer, clusterConfig.cacheKey, kubeconfig) }, } + configureFlags(cmd) return cmd } +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(idpFlag, false, "Use the STACKIT IdP for authentication to the cluster.") +} + type clusterConfig struct { - STACKITProjectID string `json:"stackitProjectId"` + STACKITProjectID string `json:"stackitProjectID"` ClusterName string `json:"clusterName"` + Region string `json:"region"` + OrganizationID string `json:"organizationID"` cacheKey string - Region string } -func parseClusterConfig(p *print.Printer, cmd *cobra.Command) (*clusterConfig, error) { +func parseClusterConfig(p *print.Printer, cmd *cobra.Command, idpMode bool) (*clusterConfig, error) { obj, _, err := exec.LoadExecCredentialFromEnv() if err != nil { return nil, fmt.Errorf("LoadExecCredentialFromEnv: %w", err) @@ -149,18 +151,52 @@ func parseClusterConfig(p *print.Printer, cmd *cobra.Command) (*clusterConfig, e if execCredential == nil || execCredential.Spec.Cluster == nil { return nil, fmt.Errorf("ExecCredential contains not all needed fields") } - config := &clusterConfig{} - err = json.Unmarshal(execCredential.Spec.Cluster.Config.Raw, config) + clusterConfig := &clusterConfig{} + err = json.Unmarshal(execCredential.Spec.Cluster.Config.Raw, clusterConfig) if err != nil { return nil, fmt.Errorf("unmarshal: %w", err) } - config.cacheKey = fmt.Sprintf("ske-login-%x", sha256.Sum256([]byte(execCredential.Spec.Cluster.Server))) + authEmail, err := auth.GetAuthEmail() + if err != nil { + return nil, fmt.Errorf("error getting auth email: %w", err) + } + idpSuffix := "" + if idpMode { + idpSuffix = "\x00idp" + } + clusterConfig.cacheKey = fmt.Sprintf("ske-login-%x", sha256.Sum256([]byte(execCredential.Spec.Cluster.Server+"\x00"+authEmail+idpSuffix))) - globalFlags := globalflags.Parse(p, cmd) - config.Region = globalFlags.Region + // NOTE: Fallback if region is not set in the kubeconfig (this was the case in the past) + if clusterConfig.Region == "" { + clusterConfig.Region = globalflags.Parse(p, cmd).Region + } - return config, nil + return clusterConfig, nil +} + +func retrieveLoginKubeconfig(ctx context.Context, apiClient *ske.APIClient, clusterConfig *clusterConfig) (*rest.Config, error) { + cachedKubeconfig := getCachedKubeConfig(clusterConfig.cacheKey) + if cachedKubeconfig == nil { + return requestNewLoginKubeconfig(ctx, apiClient, clusterConfig) + } + + isValid, notAfter := checkKubeconfigExpiry(cachedKubeconfig.CertData) + if !isValid { + // cert is expired or invalid, request new + _ = cache.DeleteObject(clusterConfig.cacheKey) + return requestNewLoginKubeconfig(ctx, apiClient, clusterConfig) + } else if time.Now().Add(refreshBeforeDuration).After(notAfter.UTC()) { + // cert expires within the next 15min -> refresh + kubeconfig, err := requestNewLoginKubeconfig(ctx, apiClient, clusterConfig) + // try to get a new one but use cache on failure + if err != nil { + return cachedKubeconfig, nil + } + return kubeconfig, nil + } + // cert not expired, nor will it expire in the next 15min; therefore, use the cached kubeconfig + return cachedKubeconfig, nil } func getCachedKubeConfig(key string) *rest.Config { @@ -177,63 +213,64 @@ func getCachedKubeConfig(key string) *rest.Config { return restConfig } -func GetAndOutputKubeconfig(ctx context.Context, p *print.Printer, apiClient *ske.APIClient, clusterConfig *clusterConfig, fallbackToCache bool, cachedKubeconfig *rest.Config) error { - req := buildRequest(ctx, apiClient, clusterConfig) - kubeconfigResponse, err := req.Execute() +func checkKubeconfigExpiry(certData []byte) (bool, time.Time) { + certPem, _ := pem.Decode(certData) + if certPem == nil { + return false, time.Time{} + } + + certificate, err := x509.ParseCertificate(certPem.Bytes) if err != nil { - if fallbackToCache { - return output(p, clusterConfig.cacheKey, cachedKubeconfig) - } - return fmt.Errorf("request kubeconfig: %w", err) + return false, time.Time{} + } + + // cert is expired + if time.Now().After(certificate.NotAfter.UTC()) { + return false, time.Time{} } + return true, certificate.NotAfter.UTC() +} +func requestNewLoginKubeconfig(ctx context.Context, apiClient *ske.APIClient, clusterConfig *clusterConfig) (*rest.Config, error) { + req := buildLoginKubeconfigRequest(ctx, apiClient, clusterConfig) + kubeconfigResponse, err := req.Execute() + if err != nil { + return nil, fmt.Errorf("request kubeconfig: %w", err) + } kubeconfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(*kubeconfigResponse.Kubeconfig)) if err != nil { - if fallbackToCache { - return output(p, clusterConfig.cacheKey, cachedKubeconfig) - } - return fmt.Errorf("parse kubeconfig: %w", err) + return nil, fmt.Errorf("parse kubeconfig: %w", err) } if err = cache.PutObject(clusterConfig.cacheKey, []byte(*kubeconfigResponse.Kubeconfig)); err != nil { - if fallbackToCache { - return output(p, clusterConfig.cacheKey, cachedKubeconfig) - } - return fmt.Errorf("cache kubeconfig: %w", err) + return nil, fmt.Errorf("cache kubeconfig: %w", err) } - return output(p, clusterConfig.cacheKey, kubeconfig) + return kubeconfig, nil } -func buildRequest(ctx context.Context, apiClient *ske.APIClient, clusterConfig *clusterConfig) ske.ApiCreateKubeconfigRequest { - req := apiClient.CreateKubeconfig(ctx, clusterConfig.STACKITProjectID, clusterConfig.Region, clusterConfig.ClusterName) +func buildLoginKubeconfigRequest(ctx context.Context, apiClient *ske.APIClient, clusterConfig *clusterConfig) ske.ApiCreateKubeconfigRequest { + req := apiClient.DefaultAPI.CreateKubeconfig(ctx, clusterConfig.STACKITProjectID, clusterConfig.Region, clusterConfig.ClusterName) expirationSeconds := strconv.Itoa(expirationSeconds) return req.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ExpirationSeconds: &expirationSeconds}) } -func output(p *print.Printer, cacheKey string, kubeconfig *rest.Config) error { - if kubeconfig == nil { - _ = cache.DeleteObject(cacheKey) - return errors.New("kubeconfig is nil") - } - - outputExecCredential, err := parseKubeConfigToExecCredential(kubeconfig) +func outputLoginKubeconfig(p *print.Printer, cacheKey string, kubeconfig *rest.Config) error { + output, err := parseLoginKubeConfigToExecCredential(kubeconfig) if err != nil { _ = cache.DeleteObject(cacheKey) return fmt.Errorf("convert to ExecCredential: %w", err) } - output, err := json.Marshal(outputExecCredential) - if err != nil { - _ = cache.DeleteObject(cacheKey) - return fmt.Errorf("marshal ExecCredential: %w", err) - } - p.Outputf("%s", string(output)) return nil } -func parseKubeConfigToExecCredential(kubeconfig *rest.Config) (*clientauthenticationv1.ExecCredential, error) { +func parseLoginKubeConfigToExecCredential(kubeconfig *rest.Config) ([]byte, error) { + if kubeconfig == nil { + return nil, errors.New("kubeconfig is nil") + } + certPem, _ := pem.Decode(kubeconfig.CertData) if certPem == nil { return nil, fmt.Errorf("decoded pem is nil") @@ -250,10 +287,127 @@ func parseKubeConfigToExecCredential(kubeconfig *rest.Config) (*clientauthentica Kind: "ExecCredential", }, Status: &clientauthenticationv1.ExecCredentialStatus{ - ExpirationTimestamp: &v1.Time{Time: certificate.NotAfter.Add(-time.Minute * 15)}, + ExpirationTimestamp: &v1.Time{Time: certificate.NotAfter.Add(-refreshBeforeDuration)}, ClientCertificateData: string(kubeconfig.CertData), ClientKeyData: string(kubeconfig.KeyData), }, } - return &outputExecCredential, nil + + output, err := json.Marshal(outputExecCredential) + if err != nil { + return nil, fmt.Errorf("marshal: %w", err) + } + return output, nil +} + +func getAccessToken(params *types.CmdParams) (string, error) { + userSessionExpired, err := auth.UserSessionExpired() + if err != nil { + return "", err + } + if userSessionExpired { + return "", &cliErr.SessionExpiredError{} + } + + accessToken, err := auth.GetValidAccessToken(params.Printer) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get valid access token: %v", err) + return "", &cliErr.SessionExpiredError{} + } + + err = auth.EnsureIDPTokenEndpoint(params.Printer) + if err != nil { + return "", err + } + + return accessToken, nil +} + +func retrieveTokenFromIDP(ctx context.Context, idpClient *http.Client, accessToken string, clusterConfig *clusterConfig) (string, error) { + resource := resourceForCluster(clusterConfig) + + cachedToken := getCachedToken(clusterConfig.cacheKey) + if cachedToken == "" { + return exchangeAndCacheToken(ctx, idpClient, accessToken, resource, clusterConfig.cacheKey) + } + + expiry, err := auth.TokenExpirationTime(cachedToken) + if err != nil { + // token is expired or invalid, request new + _ = cache.DeleteObject(clusterConfig.cacheKey) + return exchangeAndCacheToken(ctx, idpClient, accessToken, resource, clusterConfig.cacheKey) + } else if time.Now().Add(refreshTokenBeforeDuration).After(expiry) { + // token expires soon -> refresh + token, err := exchangeAndCacheToken(ctx, idpClient, accessToken, resource, clusterConfig.cacheKey) + // try to get a new one but use cache on failure + if err != nil { + return cachedToken, nil + } + return token, nil + } + // cached token is valid and won't expire soon + return cachedToken, nil +} + +func resourceForCluster(config *clusterConfig) string { + return fmt.Sprintf( + "resource://organizations/%s/projects/%s/regions/%s/ske/%s", + config.OrganizationID, + config.STACKITProjectID, + config.Region, + config.ClusterName, + ) +} + +func getCachedToken(key string) string { + token, err := cache.GetObject(key) + if err != nil { + return "" + } + return string(token) +} + +func exchangeAndCacheToken(ctx context.Context, idpClient *http.Client, accessToken, resource, cacheKey string) (string, error) { + clusterToken, err := auth.ExchangeToken(ctx, idpClient, accessToken, resource) + if err != nil { + return "", err + } + if err = cache.PutObject(cacheKey, []byte(clusterToken)); err != nil { + return "", fmt.Errorf("cache token: %w", err) + } + return clusterToken, err +} + +func outputTokenKubeconfig(p *print.Printer, cacheKey, token string) error { + output, err := parseTokenToExecCredential(token) + if err != nil { + _ = cache.DeleteObject(cacheKey) + return fmt.Errorf("convert to ExecCredential: %w", err) + } + + p.Outputf("%s", string(output)) + return nil +} + +func parseTokenToExecCredential(clusterToken string) ([]byte, error) { + expiry, err := auth.TokenExpirationTime(clusterToken) + if err != nil { + return nil, fmt.Errorf("parse auth token for cluster: %w", err) + } + + outputExecCredential := clientauthenticationv1.ExecCredential{ + TypeMeta: v1.TypeMeta{ + APIVersion: clientauthenticationv1.SchemeGroupVersion.String(), + Kind: "ExecCredential", + }, + Status: &clientauthenticationv1.ExecCredentialStatus{ + ExpirationTimestamp: &v1.Time{Time: expiry.Add(-refreshTokenBeforeDuration)}, + Token: clusterToken, + }, + } + output, err := json.Marshal(&outputExecCredential) + if err != nil { + return nil, fmt.Errorf("marshal: %w", err) + } + return output, nil } diff --git a/internal/cmd/ske/kubeconfig/login/login_test.go b/internal/cmd/ske/kubeconfig/login/login_test.go index ce22fbc1f..683171b95 100644 --- a/internal/cmd/ske/kubeconfig/login/login_test.go +++ b/internal/cmd/ske/kubeconfig/login/login_test.go @@ -2,25 +2,29 @@ package login import ( "context" + "encoding/json" "testing" "time" + "github.com/golang-jwt/jwt/v5" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" "k8s.io/client-go/rest" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} var testProjectId = uuid.NewString() var testClusterName = "cluster" +var testOrganization = uuid.NewString() const testRegion = "eu01" @@ -30,6 +34,7 @@ func fixtureClusterConfig(mods ...func(clusterConfig *clusterConfig)) *clusterCo ClusterName: testClusterName, cacheKey: "", Region: testRegion, + OrganizationID: testOrganization, } for _, mod := range mods { mod(clusterConfig) @@ -37,8 +42,8 @@ func fixtureClusterConfig(mods ...func(clusterConfig *clusterConfig)) *clusterCo return clusterConfig } -func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest { - request := testClient.CreateKubeconfig(testCtx, testProjectId, testRegion, testClusterName) +func fixtureLoginRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest { + request := testClient.DefaultAPI.CreateKubeconfig(testCtx, testProjectId, testRegion, testClusterName) request = request.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{}) for _, mod := range mods { mod(&request) @@ -55,18 +60,19 @@ func TestBuildRequest(t *testing.T) { { description: "expiration time", clusterConfig: fixtureClusterConfig(), - expectedRequest: fixtureRequest().CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ + expectedRequest: fixtureLoginRequest().CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ ExpirationSeconds: utils.Ptr("1800")}), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request := buildRequest(testCtx, testClient, tt.clusterConfig) + request := buildLoginKubeconfigRequest(testCtx, testClient, tt.clusterConfig) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -127,17 +133,78 @@ zbRjZmli7cnenEnfnNoFIGbgkbjGXRUCIC5zFtWXFK7kA+B2vDxD0DlLcQodNwi4 for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - execCredential, err := parseKubeConfigToExecCredential(tt.kubeconfig) + execCredential, err := parseLoginKubeConfigToExecCredential(tt.kubeconfig) if err != nil { t.Fatalf("func returned error: %s", err) } if execCredential == nil { t.Fatal("execCredential is nil") } - diff := cmp.Diff(execCredential, tt.expectedExecCredentialRequest) + expected, _ := json.Marshal(tt.expectedExecCredentialRequest) + diff := cmp.Diff(execCredential, expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } }) } } + +func TestParseTokenToExecCredential(t *testing.T) { + expirationTime := time.Now().Add(30 * time.Minute) + expectedTime := expirationTime.Add(-5 * time.Minute) + token, err := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expirationTime), + }).SigningString() + if err != nil { + t.Fatalf("token generation failed: %v", err) + } + token += ".signatureAAA" + + tests := []struct { + description string + token string + expectedExecCredentialRequest *clientauthenticationv1.ExecCredential + }{ + { + description: "expiration time", + token: token, + expectedExecCredentialRequest: &clientauthenticationv1.ExecCredential{ + TypeMeta: v1.TypeMeta{ + APIVersion: clientauthenticationv1.SchemeGroupVersion.String(), + Kind: "ExecCredential", + }, + Status: &clientauthenticationv1.ExecCredentialStatus{ + ExpirationTimestamp: &v1.Time{Time: expectedTime}, + Token: token, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + execCredential, err := parseTokenToExecCredential(tt.token) + if err != nil { + t.Fatalf("func returned error: %s", err) + } + if execCredential == nil { + t.Fatal("execCredential is nil") + } + expected, _ := json.Marshal(tt.expectedExecCredentialRequest) + diff := cmp.Diff(execCredential, expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestResourceForCluster(t *testing.T) { + cc := fixtureClusterConfig() + resource := resourceForCluster(cc) + // somewhat redundant, but the resource string must not change unexpectedly + expectedResource := "resource://organizations/" + testOrganization + "/projects/" + testProjectId + "/regions/" + testRegion + "/ske/" + testClusterName + if resource != expectedResource { + t.Fatalf("unexpected resource, got %v expected %v", resource, expectedResource) + } +} diff --git a/internal/cmd/ske/options/availability_zones/availability_zones.go b/internal/cmd/ske/options/availability_zones/availability_zones.go new file mode 100644 index 000000000..272da1eeb --- /dev/null +++ b/internal/cmd/ske/options/availability_zones/availability_zones.go @@ -0,0 +1,104 @@ +package availability_zones + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "availability-zones", + Short: "Lists SKE provider options for availability-zones", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for availability-zones.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for availability-zones`, + "$ stackit ske options availability-zones"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.KubernetesVersions = nil + options.MachineImages = nil + options.MachineTypes = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + zones := options.AvailabilityZones + + table := tables.NewTable() + table.SetHeader("ZONE") + for i := range zones { + z := zones[i] + table.AddRow(utils.PtrValue(z.Name)) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/availability_zones/availability_zones_test.go b/internal/cmd/ske/options/availability_zones/availability_zones_test.go new file mode 100644 index 000000000..7e57383c3 --- /dev/null +++ b/internal/cmd/ske/options/availability_zones/availability_zones_test.go @@ -0,0 +1,203 @@ +package availability_zones + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + AvailabilityZones: []ske.AvailabilityZone{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + AvailabilityZones: []ske.AvailabilityZone{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + AvailabilityZones: []ske.AvailabilityZone{ + { + Name: utils.Ptr("zone1"), + }, + { + Name: utils.Ptr("zone2"), + }, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions.go b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions.go new file mode 100644 index 000000000..65b21b4b8 --- /dev/null +++ b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions.go @@ -0,0 +1,136 @@ +package kubernetes_versions + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +const ( + supportedFlag = "supported" +) + +type inputModel struct { + globalflags.GlobalFlagModel + Supported bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "kubernetes-versions", + Short: "Lists SKE provider options for kubernetes-versions", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for kubernetes-versions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for kubernetes-versions`, + "$ stackit ske options kubernetes-versions"), + examples.NewExample( + `List SKE options for supported kubernetes-versions`, + "$ stackit ske options kubernetes-versions --supported"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(supportedFlag, false, "List supported versions only") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + Supported: flags.FlagToBoolValue(p, cmd, supportedFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region) + if model.Supported { + req = req.VersionState("SUPPORTED") + } + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.MachineImages = nil + options.MachineTypes = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + versions := options.KubernetesVersions + + table := tables.NewTable() + table.SetHeader("VERSION", "STATE", "EXPIRATION DATE", "FEATURE GATES") + for i := range versions { + v := versions[i] + featureGate, err := json.Marshal(utils.PtrValue(v.FeatureGates)) + if err != nil { + return fmt.Errorf("marshal featureGates of Kubernetes version %q: %w", utils.PtrValue(v.Version), err) + } + expirationDate := "" + if v.ExpirationDate != nil { + expirationDate = v.ExpirationDate.Format(time.RFC3339) + } + table.AddRow( + utils.PtrString(v.Version), + utils.PtrString(v.State), + expirationDate, + string(featureGate)) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions_test.go b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions_test.go new file mode 100644 index 000000000..6bb7d90d7 --- /dev/null +++ b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions_test.go @@ -0,0 +1,231 @@ +package kubernetes_versions + +import ( + "context" + "testing" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + supportedFlag: "false", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Supported: false, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + { + description: "supported only", + flagValues: map[string]string{ + supportedFlag: "true", + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Supported = true + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion), + }, + { + description: "base", + inputModel: fixtureInputModel(func(model *inputModel) { + model.Supported = true + }), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion).VersionState("SUPPORTED"), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + KubernetesVersions: []ske.KubernetesVersion{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + KubernetesVersions: []ske.KubernetesVersion{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + KubernetesVersions: []ske.KubernetesVersion{ + { + FeatureGates: &map[string]string{ + "featureGate1": "foo", + "featureGate2": "bar", + }, + State: utils.Ptr("supported"), + Version: utils.Ptr("0.00.0"), + }, + { + ExpirationDate: utils.Ptr(time.Now()), + State: utils.Ptr("deprecated"), + Version: utils.Ptr("0.00.0"), + }, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/machine_images/machine_images.go b/internal/cmd/ske/options/machine_images/machine_images.go new file mode 100644 index 000000000..94110cf6f --- /dev/null +++ b/internal/cmd/ske/options/machine_images/machine_images.go @@ -0,0 +1,128 @@ +package machine_images + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "machine-images", + Short: "Lists SKE provider options for machine-images", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for machine-images.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for machine-images`, + "$ stackit ske options machine-images"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.KubernetesVersions = nil + options.MachineTypes = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + images := options.MachineImages + + table := tables.NewTable() + table.SetHeader("NAME", "VERSION", "STATE", "EXPIRATION DATE", "SUPPORTED CRI") + for i := range images { + image := images[i] + versions := image.Versions + for j := range versions { + version := versions[j] + criNames := make([]string, 0) + for i := range version.Cri { + cri := version.Cri[i] + criNames = append(criNames, utils.PtrString(cri.Name)) + } + criNamesString := strings.Join(criNames, ", ") + + expirationDate := "-" + if version.ExpirationDate != nil { + expirationDate = version.ExpirationDate.Format(time.RFC3339) + } + table.AddRow( + utils.PtrString(image.Name), + utils.PtrString(version.Version), + utils.PtrString(version.State), + expirationDate, + criNamesString, + ) + } + } + table.EnableAutoMergeOnColumns(1) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/machine_images/machine_images_test.go b/internal/cmd/ske/options/machine_images/machine_images_test.go new file mode 100644 index 000000000..46d4d707a --- /dev/null +++ b/internal/cmd/ske/options/machine_images/machine_images_test.go @@ -0,0 +1,216 @@ +package machine_images + +import ( + "context" + "testing" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineImages: []ske.MachineImage{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineImages: []ske.MachineImage{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineImages: []ske.MachineImage{ + { + Name: utils.Ptr("image1"), + Versions: []ske.MachineImageVersion{ + { + Cri: []ske.CRI{ + { + Name: utils.Ptr("containerd"), + }, + }, + ExpirationDate: utils.Ptr(time.Now()), + State: utils.Ptr("supported"), + Version: utils.Ptr("0.00.0"), + }, + }, + }, + { + Name: utils.Ptr("zone2"), + }, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/machine_types/machine_types.go b/internal/cmd/ske/options/machine_types/machine_types.go new file mode 100644 index 000000000..7fce1c279 --- /dev/null +++ b/internal/cmd/ske/options/machine_types/machine_types.go @@ -0,0 +1,108 @@ +package machine_types + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "machine-types", + Short: "Lists SKE provider options for machine-types", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for machine-types.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for machine-types`, + "$ stackit ske options machine-types"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.KubernetesVersions = nil + options.MachineImages = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + machineTypes := options.MachineTypes + + table := tables.NewTable() + table.SetHeader("TYPE", "CPU", "MEMORY") + for i := range machineTypes { + t := machineTypes[i] + table.AddRow( + utils.PtrString(t.Name), + utils.PtrString(t.Cpu), + utils.PtrString(t.Memory), + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/machine_types/machine_types_test.go b/internal/cmd/ske/options/machine_types/machine_types_test.go new file mode 100644 index 000000000..9af525a79 --- /dev/null +++ b/internal/cmd/ske/options/machine_types/machine_types_test.go @@ -0,0 +1,211 @@ +package machine_types + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineTypes: []ske.MachineType{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineTypes: []ske.MachineType{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineTypes: []ske.MachineType{ + { + Architecture: utils.Ptr("amd64"), + Cpu: utils.Ptr(int32(2)), + Gpu: utils.Ptr(int32(0)), + Memory: utils.Ptr(int32(16)), + Name: utils.Ptr("type1"), + }, + { + Architecture: utils.Ptr("amd64"), + Cpu: utils.Ptr(int32(2)), + Gpu: utils.Ptr(int32(0)), + Memory: utils.Ptr(int32(16)), + Name: utils.Ptr("type2"), + }, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/options.go b/internal/cmd/ske/options/options.go index 91564ceea..8eb90ae9c 100644 --- a/internal/cmd/ske/options/options.go +++ b/internal/cmd/ske/options/options.go @@ -7,18 +7,23 @@ import ( "strings" "time" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/availability_zones" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/kubernetes_versions" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/machine_images" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/machine_types" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/volume_types" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/ske" ) const ( @@ -30,7 +35,7 @@ const ( ) type inputModel struct { - *globalflags.GlobalFlagModel + globalflags.GlobalFlagModel AvailabilityZones bool KubernetesVersions bool MachineImages bool @@ -38,29 +43,21 @@ type inputModel struct { VolumeTypes bool } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", Short: "Lists SKE provider options", - Long: fmt.Sprintf("%s\n%s", + Long: fmt.Sprintf("%s\n%s\n%s", + "Command \"options\" is deprecated, use the subcommands instead.", "Lists STACKIT Kubernetes Engine (SKE) provider options (availability zones, Kubernetes versions, machine images and types, volume types).", "Pass one or more flags to filter what categories are shown.", ), Args: args.NoArgs, - Example: examples.Build( - examples.NewExample( - `List SKE options for all categories`, - "$ stackit ske options"), - examples.NewExample( - `List SKE options regarding Kubernetes versions only`, - "$ stackit ske options --kubernetes-versions"), - examples.NewExample( - `List SKE options regarding Kubernetes versions and machine images`, - "$ stackit ske options --kubernetes-versions --machine-images"), - ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { + params.Printer.Info("Command \"options\" is deprecated, use the subcommands instead.\n") + ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -82,18 +79,33 @@ func NewCmd(params *params.CmdParams) *cobra.Command { }, } configureFlags(cmd) + addSubcommands(cmd, params) return cmd } +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(availability_zones.NewCmd(params)) + cmd.AddCommand(kubernetes_versions.NewCmd(params)) + cmd.AddCommand(machine_images.NewCmd(params)) + cmd.AddCommand(machine_types.NewCmd(params)) + cmd.AddCommand(volume_types.NewCmd(params)) +} + func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(availabilityZonesFlag, false, "Lists availability zones") cmd.Flags().Bool(kubernetesVersionsFlag, false, "Lists supported kubernetes versions") cmd.Flags().Bool(machineImagesFlag, false, "Lists supported machine images") cmd.Flags().Bool(machineTypesFlag, false, "Lists supported machine types") cmd.Flags().Bool(volumeTypesFlag, false, "Lists supported volume types") + + cobra.CheckErr(cmd.Flags().MarkDeprecated(availabilityZonesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the availability-zone subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(kubernetesVersionsFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the kubernetes-versions subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(machineImagesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the machine-images subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(machineTypesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the machine-types subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(volumeTypesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the volume-types subcommand instead.")) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) availabilityZones := flags.FlagToBoolValue(p, cmd, availabilityZonesFlag) kubernetesVersions := flags.FlagToBoolValue(p, cmd, kubernetesVersionsFlag) @@ -111,7 +123,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { } model := inputModel{ - GlobalFlagModel: globalFlags, + GlobalFlagModel: utils.PtrValue(globalFlags), AvailabilityZones: availabilityZones, KubernetesVersions: kubernetesVersions, MachineImages: machineImages, @@ -119,25 +131,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { VolumeTypes: volumeTypes, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { - req := apiClient.ListProviderOptions(ctx, model.Region) + req := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region) return req } func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { - if model == nil || model.GlobalFlagModel == nil { + if model == nil { return fmt.Errorf("model is nil") } else if options == nil { return fmt.Errorf("options is nil") @@ -164,25 +168,9 @@ func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOpti options.VolumeTypes = nil } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(options, "", " ") - if err != nil { - return fmt.Errorf("marshal SKE options: %w", err) - } - p.Outputln(string(details)) - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal SKE options: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(model.OutputFormat, options, func() error { return outputResultAsTable(p, options) - } + }) } func outputResultAsTable(p *print.Printer, options *ske.ProviderOptions) error { @@ -191,11 +179,11 @@ func outputResultAsTable(p *print.Printer, options *ske.ProviderOptions) error { } content := []tables.Table{} - if options.AvailabilityZones != nil && len(*options.AvailabilityZones) != 0 { + if len(options.AvailabilityZones) != 0 { content = append(content, buildAvailabilityZonesTable(options)) } - if options.KubernetesVersions != nil && len(*options.KubernetesVersions) != 0 { + if len(options.KubernetesVersions) != 0 { kubernetesVersionsTable, err := buildKubernetesVersionsTable(options) if err != nil { return fmt.Errorf("build Kubernetes versions table: %w", err) @@ -203,15 +191,15 @@ func outputResultAsTable(p *print.Printer, options *ske.ProviderOptions) error { content = append(content, kubernetesVersionsTable) } - if options.MachineImages != nil && len(*options.MachineImages) != 0 { + if len(options.MachineImages) != 0 { content = append(content, buildMachineImagesTable(options)) } - if options.MachineTypes != nil && len(*options.MachineTypes) != 0 { + if len(options.MachineTypes) != 0 { content = append(content, buildMachineTypesTable(options)) } - if options.VolumeTypes != nil && len(*options.VolumeTypes) != 0 { + if len(options.VolumeTypes) != 0 { content = append(content, buildVolumeTypesTable(options)) } @@ -224,7 +212,7 @@ func outputResultAsTable(p *print.Printer, options *ske.ProviderOptions) error { } func buildAvailabilityZonesTable(resp *ske.ProviderOptions) tables.Table { - zones := *resp.AvailabilityZones + zones := resp.AvailabilityZones table := tables.NewTable() table.SetTitle("Availability Zones") @@ -237,7 +225,7 @@ func buildAvailabilityZonesTable(resp *ske.ProviderOptions) tables.Table { } func buildKubernetesVersionsTable(resp *ske.ProviderOptions) (tables.Table, error) { - versions := *resp.KubernetesVersions + versions := resp.KubernetesVersions table := tables.NewTable() table.SetTitle("Kubernetes Versions") @@ -262,19 +250,19 @@ func buildKubernetesVersionsTable(resp *ske.ProviderOptions) (tables.Table, erro } func buildMachineImagesTable(resp *ske.ProviderOptions) tables.Table { - images := *resp.MachineImages + images := resp.MachineImages table := tables.NewTable() table.SetTitle("Machine Images") table.SetHeader("NAME", "VERSION", "STATE", "EXPIRATION DATE", "SUPPORTED CRI") for i := range images { image := images[i] - versions := *image.Versions + versions := image.Versions for j := range versions { version := versions[j] criNames := make([]string, 0) - for i := range *version.Cri { - cri := (*version.Cri)[i] + for i := range version.Cri { + cri := version.Cri[i] criNames = append(criNames, string(*cri.Name)) } criNamesString := strings.Join(criNames, ", ") @@ -297,13 +285,13 @@ func buildMachineImagesTable(resp *ske.ProviderOptions) tables.Table { } func buildMachineTypesTable(resp *ske.ProviderOptions) tables.Table { - types := *resp.MachineTypes + machineTypes := resp.MachineTypes table := tables.NewTable() table.SetTitle("Machine Types") table.SetHeader("TYPE", "CPU", "MEMORY") - for i := range types { - t := types[i] + for i := range machineTypes { + t := machineTypes[i] table.AddRow( utils.PtrString(t.Name), utils.PtrString(t.Cpu), @@ -314,13 +302,13 @@ func buildMachineTypesTable(resp *ske.ProviderOptions) tables.Table { } func buildVolumeTypesTable(resp *ske.ProviderOptions) tables.Table { - types := *resp.VolumeTypes + volumeTypes := resp.VolumeTypes table := tables.NewTable() table.SetTitle("Volume Types") table.SetHeader("TYPE") - for i := range types { - z := types[i] + for i := range volumeTypes { + z := volumeTypes[i] table.AddRow(utils.PtrString(z.Name)) } return table diff --git a/internal/cmd/ske/options/options_test.go b/internal/cmd/ske/options/options_test.go index d2317d1e5..953214bf2 100644 --- a/internal/cmd/ske/options/options_test.go +++ b/internal/cmd/ske/options/options_test.go @@ -4,19 +4,19 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &ske.APIClient{} +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} const testRegion = "eu01" @@ -37,7 +37,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModelAllFalse(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, AvailabilityZones: false, KubernetesVersions: false, MachineImages: false, @@ -52,7 +52,7 @@ func fixtureInputModelAllFalse(mods ...func(model *inputModel)) *inputModel { func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, AvailabilityZones: true, KubernetesVersions: true, MachineImages: true, @@ -68,6 +68,7 @@ func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -127,46 +128,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -178,7 +140,7 @@ func TestBuildRequest(t *testing.T) { }{ { description: "base", - expectedRequest: testClient.ListProviderOptions(testCtx, testRegion), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion), }, } @@ -189,6 +151,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -223,35 +186,35 @@ func TestOutputResult(t *testing.T) { name: "missing options", args: args{ model: &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{}, + GlobalFlagModel: globalflags.GlobalFlagModel{}, }, }, wantErr: true, }, { - name: "missing global flags in model", + name: "empty input model", args: args{ model: &inputModel{}, options: &ske.ProviderOptions{}, }, - wantErr: true, + wantErr: false, }, { name: "set model and options", args: args{ model: &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{}, + GlobalFlagModel: globalflags.GlobalFlagModel{}, }, options: &ske.ProviderOptions{}, }, wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -280,11 +243,11 @@ func TestOutputResultAsTable(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResultAsTable(p, tt.args.options); (err != nil) != tt.wantErr { + if err := outputResultAsTable(params.Printer, tt.args.options); (err != nil) != tt.wantErr { t.Errorf("outputResultAsTable() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/ske/options/volume_types/volume_types.go b/internal/cmd/ske/options/volume_types/volume_types.go new file mode 100644 index 000000000..1b4943ae4 --- /dev/null +++ b/internal/cmd/ske/options/volume_types/volume_types.go @@ -0,0 +1,104 @@ +package volume_types + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "volume-types", + Short: "Lists SKE provider options for volume-types", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for volume-types.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for volume-types`, + "$ stackit ske options volume-types"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.DefaultAPI.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.KubernetesVersions = nil + options.MachineImages = nil + options.MachineTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + volumeTypes := options.VolumeTypes + + table := tables.NewTable() + table.SetHeader("TYPE") + for i := range volumeTypes { + z := volumeTypes[i] + table.AddRow(utils.PtrString(z.Name)) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/volume_types/volume_types_test.go b/internal/cmd/ske/options/volume_types/volume_types_test.go new file mode 100644 index 000000000..a144f8419 --- /dev/null +++ b/internal/cmd/ske/options/volume_types/volume_types_test.go @@ -0,0 +1,203 @@ +package volume_types + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{DefaultAPI: &ske.DefaultAPIService{}} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.DefaultAPI.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testClient.DefaultAPI), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + VolumeTypes: []ske.VolumeType{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + VolumeTypes: []ske.VolumeType{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + VolumeTypes: []ske.VolumeType{ + { + Name: utils.Ptr("type1"), + }, + { + Name: utils.Ptr("type2"), + }, + }, + }, + }, + wantErr: false, + }, + } + params := testparams.NewTestParams() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(params.Printer, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/ske.go b/internal/cmd/ske/ske.go index e782119db..3c052fa71 100644 --- a/internal/cmd/ske/ske.go +++ b/internal/cmd/ske/ske.go @@ -1,7 +1,6 @@ package ske import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/cluster" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/credentials" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/describe" @@ -10,12 +9,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig" "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "ske", Short: "Provides functionality for SKE", @@ -27,7 +27,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(cluster.NewCmd(params)) cmd.AddCommand(credentials.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/volume/backup/backup.go b/internal/cmd/volume/backup/backup.go index b7cf8b37b..271336ba2 100644 --- a/internal/cmd/volume/backup/backup.go +++ b/internal/cmd/volume/backup/backup.go @@ -1,7 +1,6 @@ package backup import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/create" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/describe" @@ -9,12 +8,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/restore" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "backup", Short: "Provides functionality for volume backups", @@ -26,7 +26,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) cmd.AddCommand(update.NewCmd(params)) diff --git a/internal/cmd/volume/backup/create/create.go b/internal/cmd/volume/backup/create/create.go index 447a3ea16..5823edcb8 100644 --- a/internal/cmd/volume/backup/create/create.go +++ b/internal/cmd/volume/backup/create/create.go @@ -2,11 +2,10 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -20,28 +19,31 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" ) const ( - sourceIdFlag = "source-id" - sourceTypeFlag = "source-type" - nameFlag = "name" - labelsFlag = "labels" + sourceIdFlag = "source-id" + nameFlag = "name" + labelsFlag = "labels" ) -var sourceTypeFlagOptions = []string{"volume", "snapshot"} +var sourceTypeFlag = flags.StringEnumFlag( + "source-type", + []string{"volume", "snapshot"}, + "Source type of the backup,", +) type inputModel struct { *globalflags.GlobalFlagModel SourceID string SourceType string Name *string - Labels map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a backup from a specific source", @@ -58,9 +60,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a backup with labels`, "$ stackit volume backup create --source-id xxx --source-type volume --labels key1=value1,key2=value2"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -79,15 +81,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Get source name for label (use ID if name not available) sourceLabel := model.SourceID - if model.SourceType == "volume" { - name, err := iaasutils.GetVolumeName(ctx, apiClient, model.ProjectId, model.SourceID) + + switch model.SourceType { + case "volume": + name, err := iaasutils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SourceID) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) } else if name != "" { sourceLabel = name } - } else if model.SourceType == "snapshot" { - name, err := iaasutils.GetSnapshotName(ctx, apiClient, model.ProjectId, model.SourceID) + case "snapshot": + name, err := iaasutils.GetSnapshotName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SourceID) if err != nil { params.Printer.Debug(print.ErrorLevel, "get snapshot name: %v", err) } else if name != "" { @@ -95,12 +99,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", sourceLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create backup from %s? (This cannot be undone)", sourceLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -109,16 +111,19 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("create volume backup: %w", err) } + if resp == nil || resp.Id == nil { + return fmt.Errorf("create volume: empty response") + } + volumeId := *resp.Id // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating backup") - resp, err = wait.CreateBackupWaitHandler(ctx, apiClient, model.ProjectId, *resp.Id).WaitWithContext(ctx) + resp, err = spinner.Run2(params.Printer, "Creating backup", func() (*iaas.Backup, error) { + return wait.CreateBackupWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, volumeId).WaitWithContext(ctx) + }) if err != nil { return fmt.Errorf("wait for backup creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model.OutputFormat, model.Async, sourceLabel, projectLabel, resp) @@ -131,15 +136,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sourceIdFlag, "", "ID of the source from which a backup should be created") - cmd.Flags().Var(flags.EnumFlag(false, "", sourceTypeFlagOptions...), sourceTypeFlag, fmt.Sprintf("Source type of the backup, one of %q", sourceTypeFlagOptions)) + sourceTypeFlag.Register(cmd.Flags()) cmd.Flags().String(nameFlag, "", "Name of the backup") cmd.Flags().StringToString(labelsFlag, nil, "Key-value string pairs as labels") - err := flags.MarkFlagsRequired(cmd, sourceIdFlag, sourceTypeFlag) + err := flags.MarkFlagsRequired(cmd, sourceIdFlag, sourceTypeFlag.Name()) cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -150,43 +155,33 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { return nil, fmt.Errorf("source-id is required") } - sourceType := flags.FlagToStringValue(p, cmd, sourceTypeFlag) - name := flags.FlagToStringPointer(p, cmd, nameFlag) - labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + labels := flags.FlagToStringToAny(p, cmd, labelsFlag) if labels == nil { - labels = &map[string]string{} + labels = map[string]any{} } model := inputModel{ GlobalFlagModel: globalFlags, SourceID: sourceID, - SourceType: sourceType, + SourceType: sourceTypeFlag.Get(), Name: name, - Labels: *labels, - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: labels, } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateBackupRequest { - req := apiClient.CreateBackup(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreateBackup(ctx, model.ProjectId, model.Region) payload := iaas.CreateBackupPayload{ Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)), - Source: &iaas.BackupSource{ - Id: &model.SourceID, - Type: &model.SourceType, + Labels: model.Labels, + Source: iaas.BackupSource{ + Id: model.SourceID, + Type: model.SourceType, }, } @@ -198,29 +193,12 @@ func outputResult(p *print.Printer, outputFormat string, async bool, sourceLabel return fmt.Errorf("create backup response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") - if err != nil { - return fmt.Errorf("marshal backup: %w", err) - } - p.Outputln(string(details)) - return nil - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal backup: %w", err) - } - p.Outputln(string(details)) - return nil - - default: + return p.OutputResult(outputFormat, resp, func() error { if async { p.Outputf("Triggered backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, utils.PtrString(resp.Id)) } else { p.Outputf("Created backup of %s in %s. Backup ID: %s\n", sourceLabel, projectLabel, utils.PtrString(resp.Id)) } return nil - } + }) } diff --git a/internal/cmd/volume/backup/create/create_test.go b/internal/cmd/volume/backup/create/create_test.go index 3c4980cc6..3add919a4 100644 --- a/internal/cmd/volume/backup/create/create_test.go +++ b/internal/cmd/volume/backup/create/create_test.go @@ -4,39 +4,43 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -type testCtxKey struct{} - const ( + testRegion = "eu01" testName = "my-backup" testSourceType = "volume" ) +type testCtxKey struct{} + var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testSourceId = uuid.NewString() - testLabels = map[string]string{"key1": "value1"} + testLabels = map[string]any{"key1": "value1"} ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - sourceIdFlag: testSourceId, - sourceTypeFlag: testSourceType, - nameFlag: testName, - labelsFlag: "key1=value1", + globalflags.RegionFlag: testRegion, + + sourceIdFlag: testSourceId, + sourceTypeFlag.Name(): testSourceType, + nameFlag: testName, + labelsFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -49,6 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, SourceID: testSourceId, SourceType: testSourceType, @@ -62,16 +67,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateBackupRequest)) iaas.ApiCreateBackupRequest { - request := testClient.CreateBackup(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateBackup(testCtx, testProjectId, testRegion) createPayload := iaas.NewCreateBackupPayloadWithDefaults() createPayload.Name = utils.Ptr(testName) - createPayload.Labels = &map[string]interface{}{ + createPayload.Labels = map[string]any{ "key1": "value1", } - createPayload.Source = &iaas.BackupSource{ - Id: &testSourceId, - Type: utils.Ptr(testSourceType), + createPayload.Source = iaas.BackupSource{ + Id: testSourceId, + Type: testSourceType, } request = request.CreateBackupPayload(*createPayload) @@ -84,6 +89,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateBackupRequest)) iaas.Api func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -109,14 +115,14 @@ func TestParseInput(t *testing.T) { { description: "no source type", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, sourceTypeFlag) + delete(flagValues, sourceTypeFlag.Name()) }), isValid: false, }, { description: "invalid source type", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[sourceTypeFlag] = "invalid" + flagValues[sourceTypeFlag.Name()] = "invalid" }), isValid: false, }, @@ -143,53 +149,14 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { model.Name = nil - model.Labels = make(map[string]string) + model.Labels = make(map[string]any) }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -213,7 +180,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -295,13 +262,11 @@ func TestOutputResult(t *testing.T) { }, } - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - p.Cmd = cmd + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.async, tt.args.sourceLabel, tt.args.projectLabel, tt.args.backup); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.async, tt.args.sourceLabel, tt.args.projectLabel, tt.args.backup); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/backup/delete/delete.go b/internal/cmd/volume/backup/delete/delete.go index b40d93ea7..8913a2048 100644 --- a/internal/cmd/volume/backup/delete/delete.go +++ b/internal/cmd/volume/backup/delete/delete.go @@ -4,8 +4,10 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,9 +17,10 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" ) const ( @@ -29,7 +32,7 @@ type inputModel struct { BackupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", backupIdArg), Short: "Deletes a backup", @@ -52,17 +55,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) + backupLabel, err := iaasutils.GetBackupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BackupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get backup name: %v", err) } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete backup %q? (This cannot be undone)", backupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -74,13 +75,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting backup") - _, err = wait.DeleteBackupWaitHandler(ctx, apiClient, model.ProjectId, model.BackupId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting backup", func() error { + _, err = wait.DeleteBackupWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BackupId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for backup deletion: %w", err) } - s.Stop() } if model.Async { @@ -107,19 +108,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupId: backupId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteBackupRequest { - req := apiClient.DeleteBackup(ctx, model.ProjectId, model.BackupId) + req := apiClient.DefaultAPI.DeleteBackup(ctx, model.ProjectId, model.Region, model.BackupId) return req } diff --git a/internal/cmd/volume/backup/delete/delete_test.go b/internal/cmd/volume/backup/delete/delete_test.go index 8425e9c98..0950dcab7 100644 --- a/internal/cmd/volume/backup/delete/delete_test.go +++ b/internal/cmd/volume/backup/delete/delete_test.go @@ -4,21 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testBackupId = uuid.NewString() ) @@ -36,6 +39,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, BackupId: testBackupId, } @@ -58,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteBackupRequest)) iaas.ApiDeleteBackupRequest { - request := testClient.DeleteBackup(testCtx, testProjectId, testBackupId) + request := testClient.DefaultAPI.DeleteBackup(testCtx, testProjectId, testRegion, testBackupId) for _, mod := range mods { mod(&request) } @@ -110,46 +115,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -173,7 +139,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/backup/describe/describe.go b/internal/cmd/volume/backup/describe/describe.go index d3322f6d0..d3daa92ad 100644 --- a/internal/cmd/volume/backup/describe/describe.go +++ b/internal/cmd/volume/backup/describe/describe.go @@ -2,13 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { BackupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", backupIdArg), Short: "Describes a backup", @@ -83,20 +83,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupId: backupId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetBackupRequest { - req := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId) + req := apiClient.DefaultAPI.GetBackup(ctx, model.ProjectId, model.Region, model.BackupId) return req } @@ -105,24 +97,7 @@ func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) er return fmt.Errorf("backup response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backup, "", " ") - if err != nil { - return fmt.Errorf("marshal backup: %w", err) - } - p.Outputln(string(details)) - return nil - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal backup: %w", err) - } - p.Outputln(string(details)) - return nil - - default: + return p.OutputResult(outputFormat, backup, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(backup.Id)) table.AddSeparator() @@ -139,9 +114,9 @@ func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) er table.AddRow("AVAILABILITY ZONE", utils.PtrString(backup.AvailabilityZone)) table.AddSeparator() - if backup.Labels != nil && len(*backup.Labels) > 0 { + if len(backup.Labels) > 0 { var labels []string - for key, value := range *backup.Labels { + for key, value := range backup.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -158,5 +133,5 @@ func outputResult(p *print.Printer, outputFormat string, backup *iaas.Backup) er } return nil - } + }) } diff --git a/internal/cmd/volume/backup/describe/describe_test.go b/internal/cmd/volume/backup/describe/describe_test.go index 3dcac1e56..4b550d82b 100644 --- a/internal/cmd/volume/backup/describe/describe_test.go +++ b/internal/cmd/volume/backup/describe/describe_test.go @@ -4,21 +4,25 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testBackupId = uuid.NewString() ) @@ -36,6 +40,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, BackupId: testBackupId, } @@ -58,7 +64,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetBackupRequest)) iaas.ApiGetBackupRequest { - request := testClient.GetBackup(testCtx, testProjectId, testBackupId) + request := testClient.DefaultAPI.GetBackup(testCtx, testProjectId, testRegion, testBackupId) for _, mod := range mods { mod(&request) } @@ -110,46 +116,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -173,7 +140,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -205,11 +172,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.backup); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go index f3d7062c7..245050aea 100644 --- a/internal/cmd/volume/backup/list/list.go +++ b/internal/cmd/volume/backup/list/list.go @@ -2,13 +2,14 @@ package list import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,7 +20,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -33,7 +33,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all backups", @@ -53,9 +53,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List backups with specific labels`, "$ stackit volume backup list --label-selector key1=value1,key2=value2"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,23 +72,21 @@ func NewCmd(params *params.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get backups: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No backups found for project %s\n", projectLabel) - return nil + + backups := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - backups := *resp.Items // Truncate output if model.Limit != nil && len(backups) > int(*model.Limit) { backups = backups[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, backups) + return outputResult(params.Printer, model.OutputFormat, projectLabel, backups) }, } @@ -101,7 +99,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter backups by labels") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -123,20 +121,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: labelSelector, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListBackupsRequest { - req := apiClient.ListBackups(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListBackups(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) @@ -145,29 +135,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) error { - if backups == nil { - return fmt.Errorf("backups is empty") - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backups, "", " ") - if err != nil { - return fmt.Errorf("marshal backup list: %w", err) - } - p.Outputln(string(details)) - return nil - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal backup list: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, backups []iaas.Backup) error { + return p.OutputResult(outputFormat, backups, func() error { + if len(backups) == 0 { + p.Outputf("No backups found for project %s\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "SIZE", "STATUS", "SNAPSHOT ID", "VOLUME ID", "AVAILABILITY ZONE", "LABELS", "CREATED AT", "UPDATED AT") @@ -175,7 +148,7 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) var labelsString string if backup.Labels != nil { var labels []string - for key, value := range *backup.Labels { + for key, value := range backup.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } labelsString = strings.Join(labels, ", ") @@ -198,5 +171,5 @@ func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/volume/backup/list/list_test.go b/internal/cmd/volume/backup/list/list_test.go index 7a24783c2..b1183e124 100644 --- a/internal/cmd/volume/backup/list/list_test.go +++ b/internal/cmd/volume/backup/list/list_test.go @@ -4,31 +4,36 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - limitFlag: "10", - labelSelectorFlag: "key1=value1", + globalflags.RegionFlag: testRegion, + + limitFlag: "10", + labelSelectorFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -40,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Limit: utils.Ptr(int64(10)), @@ -52,7 +58,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListBackupsRequest)) iaas.ApiListBackupsRequest { - request := testClient.ListBackups(testCtx, testProjectId) + request := testClient.DefaultAPI.ListBackups(testCtx, testProjectId, testRegion) request = request.LabelSelector("key1=value1") for _, mod := range mods { mod(&request) @@ -63,6 +69,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListBackupsRequest)) iaas.ApiL func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -117,48 +124,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - configureFlags(cmd) - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - p := print.NewPrinter() - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -182,7 +148,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -194,6 +160,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string backups []iaas.Backup } tests := []struct { @@ -204,7 +171,7 @@ func TestOutputResult(t *testing.T) { { name: "empty", args: args{}, - wantErr: true, + wantErr: false, }, { name: "empty backup in slice", @@ -221,11 +188,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.backups); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.backups); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/backup/restore/restore.go b/internal/cmd/volume/backup/restore/restore.go index 3249f0560..842d86921 100644 --- a/internal/cmd/volume/backup/restore/restore.go +++ b/internal/cmd/volume/backup/restore/restore.go @@ -4,8 +4,10 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -15,9 +17,10 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" ) const ( @@ -29,7 +32,7 @@ type inputModel struct { BackupId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("restore %s", backupIdArg), Short: "Restores a backup", @@ -52,17 +55,17 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) + backupLabel, err := iaasutils.GetBackupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BackupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get backup details: %v", err) } // Get source details for labels var sourceLabel string - backup, err := apiClient.GetBackup(ctx, model.ProjectId, model.BackupId).Execute() + backup, err := apiClient.DefaultAPI.GetBackup(ctx, model.ProjectId, model.Region, model.BackupId).Execute() if err == nil && backup != nil && backup.VolumeId != nil { sourceLabel = *backup.VolumeId - name, err := iaasutils.GetVolumeName(ctx, apiClient, model.ProjectId, *backup.VolumeId) + name, err := iaasutils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *backup.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume details: %v", err) } else if name != "" { @@ -70,12 +73,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to restore %q with backup %q? (This cannot be undone)", sourceLabel, backupLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to restore %q with backup %q? (This cannot be undone)", sourceLabel, backupLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -87,13 +88,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Restoring backup") - _, err = wait.RestoreBackupWaitHandler(ctx, apiClient, model.ProjectId, model.BackupId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Restoring backup", func() error { + _, err = wait.RestoreBackupWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BackupId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for backup restore: %w", err) } - s.Stop() } if model.Async { @@ -120,19 +121,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu BackupId: backupId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiRestoreBackupRequest { - req := apiClient.RestoreBackup(ctx, model.ProjectId, model.BackupId) + req := apiClient.DefaultAPI.RestoreBackup(ctx, model.ProjectId, model.Region, model.BackupId) return req } diff --git a/internal/cmd/volume/backup/restore/restore_test.go b/internal/cmd/volume/backup/restore/restore_test.go index 217300720..2d666666c 100644 --- a/internal/cmd/volume/backup/restore/restore_test.go +++ b/internal/cmd/volume/backup/restore/restore_test.go @@ -4,21 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testBackupId = uuid.NewString() ) @@ -36,6 +39,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, BackupId: testBackupId, } @@ -58,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiRestoreBackupRequest)) iaas.ApiRestoreBackupRequest { - request := testClient.RestoreBackup(testCtx, testProjectId, testBackupId) + request := testClient.DefaultAPI.RestoreBackup(testCtx, testProjectId, testRegion, testBackupId) for _, mod := range mods { mod(&request) } @@ -110,46 +115,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -173,7 +139,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/backup/update/update.go b/internal/cmd/volume/backup/update/update.go index f23bb5108..1ec7cf927 100644 --- a/internal/cmd/volume/backup/update/update.go +++ b/internal/cmd/volume/backup/update/update.go @@ -2,12 +2,12 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,7 @@ import ( iaasutils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -31,10 +31,10 @@ type inputModel struct { *globalflags.GlobalFlagModel BackupId string Name *string - Labels map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", backupIdArg), Short: "Updates a backup", @@ -61,17 +61,15 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - backupLabel, err := iaasutils.GetBackupName(ctx, apiClient, model.ProjectId, model.BackupId) + backupLabel, err := iaasutils.GetBackupName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.BackupId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get backup name: %v", err) } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update backup %q? (This cannot be undone)", model.BackupId) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update backup %q? (This cannot be undone)", model.BackupId) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -103,36 +101,28 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } name := flags.FlagToStringPointer(p, cmd, nameFlag) - labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + labels := flags.FlagToStringToAny(p, cmd, labelsFlag) if labels == nil { - labels = &map[string]string{} + labels = map[string]any{} } model := inputModel{ GlobalFlagModel: globalFlags, BackupId: backupId, Name: name, - Labels: *labels, - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: labels, } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateBackupRequest { - req := apiClient.UpdateBackup(ctx, model.ProjectId, model.BackupId) + req := apiClient.DefaultAPI.UpdateBackup(ctx, model.ProjectId, model.Region, model.BackupId) payload := iaas.UpdateBackupPayload{ Name: model.Name, - Labels: utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)), + Labels: model.Labels, } req = req.UpdateBackupPayload(payload) @@ -144,25 +134,8 @@ func outputResult(p *print.Printer, outputFormat, backupLabel string, backup *ia return fmt.Errorf("backup response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(backup, "", " ") - if err != nil { - return fmt.Errorf("marshal backup: %w", err) - } - p.Outputln(string(details)) - return nil - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal backup: %w", err) - } - p.Outputln(string(details)) - return nil - - default: + return p.OutputResult(outputFormat, backup, func() error { p.Outputf("Updated backup %q\n", backupLabel) return nil - } + }) } diff --git a/internal/cmd/volume/backup/update/update_test.go b/internal/cmd/volume/backup/update/update_test.go index 21aa21c1e..585e8117b 100644 --- a/internal/cmd/volume/backup/update/update_test.go +++ b/internal/cmd/volume/backup/update/update_test.go @@ -4,26 +4,29 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" + testName = "test-backup" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testBackupId = uuid.NewString() - testName = "test-backup" - testLabels = map[string]string{"key1": "value1"} + testLabels = map[string]any{"key1": "value1"} ) func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -39,8 +42,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - nameFlag: testName, - labelsFlag: "key1=value1", + globalflags.RegionFlag: testRegion, + + nameFlag: testName, + labelsFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -53,9 +58,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, BackupId: testBackupId, - Name: &testName, + Name: utils.Ptr(testName), Labels: testLabels, } for _, mod := range mods { @@ -65,11 +71,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateBackupRequest)) iaas.ApiUpdateBackupRequest { - request := testClient.UpdateBackup(testCtx, testProjectId, testBackupId) + request := testClient.DefaultAPI.UpdateBackup(testCtx, testProjectId, testRegion, testBackupId) payload := iaas.NewUpdateBackupPayloadWithDefaults() - payload.Name = &testName + payload.Name = utils.Ptr(testName) - payload.Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(testLabels)) + payload.Labels = testLabels request = request.UpdateBackupPayload(*payload) for _, mod := range mods { @@ -115,46 +121,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -178,7 +145,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/create/create.go b/internal/cmd/volume/create/create.go index 5519bd5d8..767f69e5f 100644 --- a/internal/cmd/volume/create/create.go +++ b/internal/cmd/volume/create/create.go @@ -2,11 +2,13 @@ package create import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,8 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -36,17 +36,17 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel - AvailabilityZone *string + AvailabilityZone string Name *string Description *string - Labels *map[string]string + Labels map[string]any PerformanceClass *string Size *int64 SourceId *string SourceType *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a volume", @@ -70,9 +70,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `$ stackit volume create --availability-zone eu01-1 --performance-class storage_premium_perf1 --size 64`, ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -89,12 +89,10 @@ func NewCmd(params *params.CmdParams) *cobra.Command { projectLabel = model.ProjectId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create a volume for project %q?", projectLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create a volume for project %q?", projectLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -107,13 +105,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating volume") - _, err = wait.CreateVolumeWaitHandler(ctx, apiClient, model.ProjectId, volumeId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Creating volume", func() error { + _, err = wait.CreateVolumeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, volumeId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for volume creation: %w", err) } - s.Stop() } return outputResult(params.Printer, model, projectLabel, resp) @@ -137,7 +135,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &cliErr.ProjectIdError{} @@ -145,46 +143,37 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, - AvailabilityZone: flags.FlagToStringPointer(p, cmd, availabilityZoneFlag), + AvailabilityZone: flags.FlagToStringValue(p, cmd, availabilityZoneFlag), Name: flags.FlagToStringPointer(p, cmd, nameFlag), Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), PerformanceClass: flags.FlagToStringPointer(p, cmd, performanceClassFlag), Size: flags.FlagToInt64Pointer(p, cmd, sizeFlag), SourceId: flags.FlagToStringPointer(p, cmd, sourceIdFlag), SourceType: flags.FlagToStringPointer(p, cmd, sourceTypeFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateVolumeRequest { - req := apiClient.CreateVolume(ctx, model.ProjectId) - source := &iaas.VolumeSource{ - Id: model.SourceId, - Type: model.SourceType, - } + req := apiClient.DefaultAPI.CreateVolume(ctx, model.ProjectId, model.Region) payload := iaas.CreateVolumePayload{ AvailabilityZone: model.AvailabilityZone, Name: model.Name, Description: model.Description, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, PerformanceClass: model.PerformanceClass, Size: model.Size, } if model.SourceId != nil && model.SourceType != nil { - payload.Source = source + payload.Source = &iaas.VolumeSource{ + Id: *model.SourceId, + Type: *model.SourceType, + } } return req.CreateVolumePayload(payload) @@ -194,25 +183,12 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, volu if volume == nil { return fmt.Errorf("volume response is empty") } - switch model.OutputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volume, "", " ") - if err != nil { - return fmt.Errorf("marshal volume: %w", err) + return p.OutputResult(model.OutputFormat, volume, func() error { + operationState := "Created" + if model.Async { + operationState = "Triggered creation of" } - p.Outputln(string(details)) - + p.Outputf("%s volume for project %q.\nVolume ID: %s\n", operationState, projectLabel, utils.PtrString(volume.Id)) return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volume, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: - p.Outputf("Created volume for project %q.\nVolume ID: %s\n", projectLabel, utils.PtrString(volume.Id)) - return nil - } + }) } diff --git a/internal/cmd/volume/create/create_test.go b/internal/cmd/volume/create/create_test.go index 716daaad2..3848a5e1f 100644 --- a/internal/cmd/volume/create/create_test.go +++ b/internal/cmd/volume/create/create_test.go @@ -7,26 +7,31 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testSourceId = uuid.NewString() func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + availabilityZoneFlag: "eu01-1", nameFlag: "example-volume-name", descriptionFlag: "example-volume-description", @@ -47,17 +52,18 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - AvailabilityZone: utils.Ptr("eu01-1"), + AvailabilityZone: "eu01-1", Name: utils.Ptr("example-volume-name"), Description: utils.Ptr("example-volume-description"), PerformanceClass: utils.Ptr("example-perf-class"), Size: utils.Ptr(int64(5)), - SourceId: utils.Ptr(testSourceId), + SourceId: &testSourceId, SourceType: utils.Ptr("example-source-type"), - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -66,7 +72,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateVolumeRequest)) iaas.ApiCreateVolumeRequest { - request := testClient.CreateVolume(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateVolume(testCtx, testProjectId, testRegion) request = request.CreateVolumePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -75,9 +81,9 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateVolumeRequest)) iaas.Api } func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateVolumeRequest)) iaas.ApiCreateVolumeRequest { - request := testClient.CreateVolume(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateVolume(testCtx, testProjectId, testRegion) request = request.CreateVolumePayload(iaas.CreateVolumePayload{ - AvailabilityZone: utils.Ptr("eu01-1"), + AvailabilityZone: "eu01-1", }) for _, mod := range mods { mod(&request) @@ -87,17 +93,17 @@ func fixtureRequiredRequest(mods ...func(request *iaas.ApiCreateVolumeRequest)) func fixturePayload(mods ...func(payload *iaas.CreateVolumePayload)) iaas.CreateVolumePayload { payload := iaas.CreateVolumePayload{ - AvailabilityZone: utils.Ptr("eu01-1"), + AvailabilityZone: "eu01-1", Name: utils.Ptr("example-volume-name"), Description: utils.Ptr("example-volume-description"), PerformanceClass: utils.Ptr("example-perf-class"), Size: utils.Ptr(int64(5)), - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), + }, Source: &iaas.VolumeSource{ - Id: utils.Ptr(testSourceId), - Type: utils.Ptr("example-source-type"), + Id: testSourceId, + Type: "example-source-type", }, } for _, mod := range mods { @@ -109,6 +115,7 @@ func fixturePayload(mods ...func(payload *iaas.CreateVolumePayload)) iaas.Create func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -156,21 +163,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -194,7 +201,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.SourceId = utils.Ptr(testSourceId) + model.SourceId = &testSourceId model.SourceType = utils.Ptr("example-source-type") }), }, @@ -202,46 +209,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing flags: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -263,8 +231,9 @@ func TestBuildRequest(t *testing.T) { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, - AvailabilityZone: utils.Ptr("eu01-1"), + AvailabilityZone: "eu01-1", }, expectedRequest: fixtureRequiredRequest(), }, @@ -276,7 +245,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -310,11 +279,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.model, tt.args.projectLabel, tt.args.volume); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.model, tt.args.projectLabel, tt.args.volume); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/delete/delete.go b/internal/cmd/volume/delete/delete.go index 117c17032..f094ff20e 100644 --- a/internal/cmd/volume/delete/delete.go +++ b/internal/cmd/volume/delete/delete.go @@ -4,7 +4,11 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +18,6 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/spf13/cobra" ) @@ -29,7 +31,7 @@ type inputModel struct { VolumeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", volumeIdArg), Short: "Deletes a volume", @@ -57,18 +59,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete volume %q?", volumeLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete volume %q?", volumeLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -80,13 +80,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting volume") - _, err = wait.DeleteVolumeWaitHandler(ctx, apiClient, model.ProjectId, model.VolumeId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting volume", func() error { + _, err = wait.DeleteVolumeWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for volume deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -113,18 +113,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteVolumeRequest { - return apiClient.DeleteVolume(ctx, model.ProjectId, model.VolumeId) + return apiClient.DefaultAPI.DeleteVolume(ctx, model.ProjectId, model.Region, model.VolumeId) } diff --git a/internal/cmd/volume/delete/delete_test.go b/internal/cmd/volume/delete/delete_test.go index 58d10c79c..6408764fb 100644 --- a/internal/cmd/volume/delete/delete_test.go +++ b/internal/cmd/volume/delete/delete_test.go @@ -4,22 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testVolumeId = uuid.NewString() var testProjectId = uuid.NewString() @@ -35,7 +36,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +50,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, VolumeId: testVolumeId, } @@ -58,7 +61,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteVolumeRequest)) iaas.ApiDeleteVolumeRequest { - request := testClient.DeleteVolume(testCtx, testProjectId, testVolumeId) + request := testClient.DefaultAPI.DeleteVolume(testCtx, testProjectId, testRegion, testVolumeId) for _, mod := range mods { mod(&request) } @@ -102,7 +105,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -110,7 +113,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -118,7 +121,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -138,54 +141,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -209,7 +165,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/describe/describe.go b/internal/cmd/volume/describe/describe.go index 9d4c06538..1ac849084 100644 --- a/internal/cmd/volume/describe/describe.go +++ b/internal/cmd/volume/describe/describe.go @@ -2,13 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -31,7 +30,7 @@ type inputModel struct { VolumeId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", volumeIdArg), Short: "Shows details of a volume", @@ -86,44 +85,19 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetVolumeRequest { - return apiClient.GetVolume(ctx, model.ProjectId, model.VolumeId) + return apiClient.DefaultAPI.GetVolume(ctx, model.ProjectId, model.Region, model.VolumeId) } func outputResult(p *print.Printer, outputFormat string, volume *iaas.Volume) error { if volume == nil { return fmt.Errorf("volume response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volume, "", " ") - if err != nil { - return fmt.Errorf("marshal volume: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volume, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, volume, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(volume.Id)) table.AddSeparator() @@ -135,11 +109,11 @@ func outputResult(p *print.Printer, outputFormat string, volume *iaas.Volume) er table.AddSeparator() table.AddRow("PERFORMANCE CLASS", utils.PtrString(volume.PerformanceClass)) table.AddSeparator() - table.AddRow("AVAILABILITY ZONE", utils.PtrString(volume.AvailabilityZone)) + table.AddRow("AVAILABILITY ZONE", volume.AvailabilityZone) table.AddSeparator() if volume.Source != nil { - sourceId := *volume.Source.Id + sourceId := volume.Source.Id table.AddRow("SOURCE", sourceId) table.AddSeparator() } @@ -150,9 +124,9 @@ func outputResult(p *print.Printer, outputFormat string, volume *iaas.Volume) er table.AddSeparator() } - if volume.Labels != nil && len(*volume.Labels) > 0 { + if len(volume.Labels) > 0 { labels := []string{} - for key, value := range *volume.Labels { + for key, value := range volume.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -163,5 +137,5 @@ func outputResult(p *print.Printer, outputFormat string, volume *iaas.Volume) er return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/volume/describe/describe_test.go b/internal/cmd/volume/describe/describe_test.go index aa0a0754f..70ba621f0 100644 --- a/internal/cmd/volume/describe/describe_test.go +++ b/internal/cmd/volume/describe/describe_test.go @@ -7,18 +7,21 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -34,7 +37,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, Verbosity: globalflags.VerbosityDefault, + Region: testRegion, }, VolumeId: testVolumeId, } @@ -57,7 +62,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetVolumeRequest)) iaas.ApiGetVolumeRequest { - request := testClient.GetVolume(testCtx, testProjectId, testVolumeId) + request := testClient.DefaultAPI.GetVolume(testCtx, testProjectId, testRegion, testVolumeId) for _, mod := range mods { mod(&request) } @@ -101,7 +106,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -109,7 +114,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -117,7 +122,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -137,54 +142,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -208,7 +166,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -240,11 +198,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.volume); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.volume); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/list/list.go b/internal/cmd/volume/list/list.go index aac08c50b..cf2cb500c 100644 --- a/internal/cmd/volume/list/list.go +++ b/internal/cmd/volume/list/list.go @@ -2,12 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +19,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -32,7 +32,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all volumes of a project", @@ -56,9 +56,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit volume list --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -76,23 +76,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list volumes: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No volumes found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -104,7 +101,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -124,20 +121,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListVolumesRequest { - req := apiClient.ListVolumes(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListVolumes(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } @@ -145,25 +134,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, volumes []iaas.Volume) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volumes, "", " ") - if err != nil { - return fmt.Errorf("marshal volume: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, volumes []iaas.Volume) error { + return p.OutputResult(outputFormat, volumes, func() error { + if len(volumes) == 0 { + p.Outputf("No volumes found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volumes, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("ID", "Name", "Status", "Server", "Availability Zone", "Size (GB)") @@ -174,7 +150,7 @@ func outputResult(p *print.Printer, outputFormat string, volumes []iaas.Volume) utils.PtrString(volume.Name), utils.PtrString(volume.Status), utils.PtrString(volume.ServerId), - utils.PtrString(volume.AvailabilityZone), + volume.AvailabilityZone, utils.PtrString(volume.Size), ) table.AddSeparator() @@ -182,5 +158,5 @@ func outputResult(p *print.Printer, outputFormat string, volumes []iaas.Volume) p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/volume/list/list_test.go b/internal/cmd/volume/list/list_test.go index daa736ff1..65667f341 100644 --- a/internal/cmd/volume/list/list_test.go +++ b/internal/cmd/volume/list/list_test.go @@ -7,25 +7,30 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testLabelSelector = "label" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", labelSelectorFlag: testLabelSelector, } @@ -40,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), @@ -51,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListVolumesRequest)) iaas.ApiListVolumesRequest { - request := testClient.ListVolumes(testCtx, testProjectId) + request := testClient.DefaultAPI.ListVolumes(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -62,6 +68,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListVolumesRequest)) iaas.ApiL func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -85,21 +92,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -131,46 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -194,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -206,6 +174,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string volumes []iaas.Volume } tests := []struct { @@ -226,11 +195,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.volumes); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.volumes); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/performance-class/describe/describe.go b/internal/cmd/volume/performance-class/describe/describe.go index dc7b25ad5..617dcfd99 100644 --- a/internal/cmd/volume/performance-class/describe/describe.go +++ b/internal/cmd/volume/performance-class/describe/describe.go @@ -2,13 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -31,7 +30,7 @@ type inputModel struct { VolumePerformanceClass string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", volumePerformanceClassArg), Short: "Shows details of a volume performance class", @@ -86,46 +85,21 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu VolumePerformanceClass: volumePerformanceClass, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetVolumePerformanceClassRequest { - return apiClient.GetVolumePerformanceClass(ctx, model.ProjectId, model.VolumePerformanceClass) + return apiClient.DefaultAPI.GetVolumePerformanceClass(ctx, model.ProjectId, model.Region, model.VolumePerformanceClass) } func outputResult(p *print.Printer, outputFormat string, performanceClass *iaas.VolumePerformanceClass) error { if performanceClass == nil { return fmt.Errorf("performanceClass response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(performanceClass, "", " ") - if err != nil { - return fmt.Errorf("marshal volume performance class: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(performanceClass, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal volume performance class: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, performanceClass, func() error { table := tables.NewTable() - table.AddRow("NAME", utils.PtrString(performanceClass.Name)) + table.AddRow("NAME", performanceClass.Name) table.AddSeparator() table.AddRow("DESCRIPTION", utils.PtrString(performanceClass.Description)) table.AddSeparator() @@ -134,9 +108,9 @@ func outputResult(p *print.Printer, outputFormat string, performanceClass *iaas. table.AddRow("THROUGHPUT", utils.PtrString(performanceClass.Throughput)) table.AddSeparator() - if performanceClass.Labels != nil && len(*performanceClass.Labels) > 0 { + if len(performanceClass.Labels) > 0 { labels := []string{} - for key, value := range *performanceClass.Labels { + for key, value := range performanceClass.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -148,5 +122,5 @@ func outputResult(p *print.Printer, outputFormat string, performanceClass *iaas. return fmt.Errorf("render table: %w", err) } return nil - } + }) } diff --git a/internal/cmd/volume/performance-class/describe/describe_test.go b/internal/cmd/volume/performance-class/describe/describe_test.go index b5bf0cf3c..607b5ccd4 100644 --- a/internal/cmd/volume/performance-class/describe/describe_test.go +++ b/internal/cmd/volume/performance-class/describe/describe_test.go @@ -4,21 +4,25 @@ import ( "context" "testing" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testVolumePerformanceClass = "storage_premium_perf6" @@ -34,7 +38,8 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -46,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, VolumePerformanceClass: testVolumePerformanceClass, @@ -57,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetVolumePerformanceClassRequest)) iaas.ApiGetVolumePerformanceClassRequest { - request := testClient.GetVolumePerformanceClass(testCtx, testProjectId, testVolumePerformanceClass) + request := testClient.DefaultAPI.GetVolumePerformanceClass(testCtx, testProjectId, testRegion, testVolumePerformanceClass) for _, mod := range mods { mod(&request) } @@ -101,7 +107,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -109,7 +115,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -117,7 +123,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -131,54 +137,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -202,7 +161,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -234,11 +193,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.performanceClass); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.performanceClass); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/performance-class/list/list.go b/internal/cmd/volume/performance-class/list/list.go index a6490e75e..830fbbb09 100644 --- a/internal/cmd/volume/performance-class/list/list.go +++ b/internal/cmd/volume/performance-class/list/list.go @@ -2,11 +2,12 @@ package list import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -17,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -33,7 +33,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all volume performance classes for a project", @@ -57,9 +57,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { "$ stackit volume performance-class list --limit 10", ), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -77,23 +77,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list volume performance classes: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No volume performance class found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -105,7 +102,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -125,20 +122,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListVolumePerformanceClassesRequest { - req := apiClient.ListVolumePerformanceClasses(ctx, model.ProjectId) + req := apiClient.DefaultAPI.ListVolumePerformanceClasses(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } @@ -146,34 +135,21 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, performanceClasses []iaas.VolumePerformanceClass) error { - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(performanceClasses, "", " ") - if err != nil { - return fmt.Errorf("marshal volume performance class: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, performanceClasses []iaas.VolumePerformanceClass) error { + return p.OutputResult(outputFormat, performanceClasses, func() error { + if len(performanceClasses) == 0 { + p.Outputf("No volume performance class found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(performanceClasses, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal volume performance class: %w", err) - } - p.Outputln(string(details)) - - return nil - default: table := tables.NewTable() table.SetHeader("Name", "Description") for _, performanceClass := range performanceClasses { - table.AddRow(utils.PtrString(performanceClass.Name), utils.PtrString(performanceClass.Description)) + table.AddRow(performanceClass.Name, utils.PtrString(performanceClass.Description)) table.AddSeparator() } p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/volume/performance-class/list/list_test.go b/internal/cmd/volume/performance-class/list/list_test.go index 6e877390d..aac385672 100644 --- a/internal/cmd/volume/performance-class/list/list_test.go +++ b/internal/cmd/volume/performance-class/list/list_test.go @@ -7,25 +7,30 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testLabelSelector = "label" func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + limitFlag: "10", labelSelectorFlag: testLabelSelector, } @@ -40,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { GlobalFlagModel: &globalflags.GlobalFlagModel{ Verbosity: globalflags.VerbosityDefault, ProjectId: testProjectId, + Region: testRegion, }, Limit: utils.Ptr(int64(10)), LabelSelector: utils.Ptr(testLabelSelector), @@ -51,7 +57,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiListVolumePerformanceClassesRequest)) iaas.ApiListVolumePerformanceClassesRequest { - request := testClient.ListVolumePerformanceClasses(testCtx, testProjectId) + request := testClient.DefaultAPI.ListVolumePerformanceClasses(testCtx, testProjectId, testRegion) request = request.LabelSelector(testLabelSelector) for _, mod := range mods { mod(&request) @@ -62,6 +68,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListVolumePerformanceClassesRe func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -85,21 +92,21 @@ func TestParseInput(t *testing.T) { { description: "project id missing", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, { description: "project id invalid 1", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, { description: "project id invalid 2", flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -131,46 +138,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -194,7 +162,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -206,6 +174,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string performanceClasses []iaas.VolumePerformanceClass } tests := []struct { @@ -226,11 +195,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.performanceClasses); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.performanceClasses); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/performance-class/performance_class.go b/internal/cmd/volume/performance-class/performance_class.go index 6ed86618a..dd00fe2d6 100644 --- a/internal/cmd/volume/performance-class/performance_class.go +++ b/internal/cmd/volume/performance-class/performance_class.go @@ -1,16 +1,16 @@ package performanceclass import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/performance-class/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/performance-class/list" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "performance-class", Short: "Provides functionality for volume performance classes available inside a project", @@ -22,7 +22,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(describe.NewCmd(params)) cmd.AddCommand(list.NewCmd(params)) } diff --git a/internal/cmd/volume/resize/resize.go b/internal/cmd/volume/resize/resize.go index f86f10546..c34b6d41e 100644 --- a/internal/cmd/volume/resize/resize.go +++ b/internal/cmd/volume/resize/resize.go @@ -4,7 +4,10 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,7 +17,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/spf13/cobra" ) @@ -28,10 +30,10 @@ const ( type inputModel struct { *globalflags.GlobalFlagModel VolumeId string - Size *int64 + Size int64 } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("resize %s", volumeIdArg), Short: "Resizes a volume", @@ -56,18 +58,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to resize volume %q?", volumeLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to resize volume %q?", volumeLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -102,24 +102,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu model := inputModel{ GlobalFlagModel: globalFlags, - Size: flags.FlagToInt64Pointer(p, cmd, sizeFlag), + Size: *flags.FlagToInt64Pointer(p, cmd, sizeFlag), VolumeId: volumeId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiResizeVolumeRequest { - req := apiClient.ResizeVolume(ctx, model.ProjectId, model.VolumeId) + req := apiClient.DefaultAPI.ResizeVolume(ctx, model.ProjectId, model.Region, model.VolumeId) payload := iaas.ResizeVolumePayload{ Size: model.Size, diff --git a/internal/cmd/volume/resize/resize_test.go b/internal/cmd/volume/resize/resize_test.go index 54d02a47f..0e7cbaf69 100644 --- a/internal/cmd/volume/resize/resize_test.go +++ b/internal/cmd/volume/resize/resize_test.go @@ -4,23 +4,23 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" - "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -37,8 +37,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ - sizeFlag: "10", - projectIdFlag: testProjectId, + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + + sizeFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -50,9 +52,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, - Size: utils.Ptr(int64(10)), + Size: int64(10), VolumeId: testVolumeId, } for _, mod := range mods { @@ -62,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiResizeVolumeRequest)) iaas.ApiResizeVolumeRequest { - request := testClient.ResizeVolume(testCtx, testProjectId, testVolumeId) + request := testClient.DefaultAPI.ResizeVolume(testCtx, testProjectId, testRegion, testVolumeId) request = request.ResizeVolumePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -72,7 +75,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiResizeVolumeRequest)) iaas.Api func fixturePayload(mods ...func(payload *iaas.ResizeVolumePayload)) iaas.ResizeVolumePayload { payload := iaas.ResizeVolumePayload{ - Size: utils.Ptr(int64(10)), + Size: int64(10), } for _, mod := range mods { mod(&payload) @@ -105,7 +108,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -113,7 +116,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -121,7 +124,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -145,15 +148,15 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Size = utils.Ptr(int64(15)) + model.Size = int64(15) }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -185,7 +188,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -223,7 +226,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/snapshot/create/create.go b/internal/cmd/volume/snapshot/create/create.go index 856b0a929..68ce82eb6 100644 --- a/internal/cmd/volume/snapshot/create/create.go +++ b/internal/cmd/volume/snapshot/create/create.go @@ -4,8 +4,10 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,8 +20,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" ) const ( @@ -32,10 +34,10 @@ type inputModel struct { *globalflags.GlobalFlagModel VolumeID string Name *string - Labels map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Creates a snapshot from a volume", @@ -52,9 +54,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `Create a snapshot from a volume with ID "xxx" and labels`, "$ stackit volume snapshot create --volume-id xxx --labels key1=value1,key2=value2"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -72,18 +74,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Get volume name for label - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeID) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeID) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeID } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to create snapshot from volume %q? (This cannot be undone)", volumeLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to create snapshot from volume %q? (This cannot be undone)", volumeLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -95,13 +95,12 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Creating snapshot") - resp, err = wait.CreateSnapshotWaitHandler(ctx, apiClient, model.ProjectId, *resp.Id).WaitWithContext(ctx) + resp, err = spinner.Run2(params.Printer, "Creating snapshot", func() (*iaas.Snapshot, error) { + return wait.CreateSnapshotWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, *resp.Id).WaitWithContext(ctx) + }) if err != nil { return fmt.Errorf("wait for snapshot creation: %w", err) } - s.Stop() } operationState := "Created" @@ -126,7 +125,7 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -135,36 +134,28 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { volumeID := flags.FlagToStringValue(p, cmd, volumeIdFlag) name := flags.FlagToStringPointer(p, cmd, nameFlag) - labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + labels := flags.FlagToStringToAny(p, cmd, labelsFlag) if labels == nil { - labels = &map[string]string{} + labels = map[string]any{} } model := inputModel{ GlobalFlagModel: globalFlags, VolumeID: volumeID, Name: name, - Labels: *labels, - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: labels, } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiCreateSnapshotRequest { - req := apiClient.CreateSnapshot(ctx, model.ProjectId) + req := apiClient.DefaultAPI.CreateSnapshot(ctx, model.ProjectId, model.Region) payload := iaas.NewCreateSnapshotPayloadWithDefaults() - payload.VolumeId = &model.VolumeID + payload.VolumeId = model.VolumeID payload.Name = model.Name - payload.Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)) + payload.Labels = model.Labels req = req.CreateSnapshotPayload(*payload) return req diff --git a/internal/cmd/volume/snapshot/create/create_test.go b/internal/cmd/volume/snapshot/create/create_test.go index 08c5c0baa..ac2744f9d 100644 --- a/internal/cmd/volume/snapshot/create/create_test.go +++ b/internal/cmd/volume/snapshot/create/create_test.go @@ -7,33 +7,36 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type testCtxKey struct{} const ( - testName = "test-snapshot" + testRegion = "eu01" + testName = "test-snapshot" ) var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testVolumeId = uuid.NewString() - testLabels = map[string]string{"key1": "value1"} + testLabels = map[string]any{"key1": "value1"} ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - volumeIdFlag: testVolumeId, - nameFlag: testName, - labelsFlag: "key1=value1", + globalflags.RegionFlag: testRegion, + + volumeIdFlag: testVolumeId, + nameFlag: testName, + labelsFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -45,6 +48,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, VolumeID: testVolumeId, @@ -58,12 +62,12 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiCreateSnapshotRequest)) iaas.ApiCreateSnapshotRequest { - request := testClient.CreateSnapshot(testCtx, testProjectId) + request := testClient.DefaultAPI.CreateSnapshot(testCtx, testProjectId, testRegion) payload := iaas.NewCreateSnapshotPayloadWithDefaults() - payload.VolumeId = &testVolumeId + payload.VolumeId = testVolumeId payload.Name = utils.Ptr(testName) - payload.Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(testLabels)) + payload.Labels = testLabels request = request.CreateSnapshotPayload(*payload) for _, mod := range mods { @@ -75,6 +79,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiCreateSnapshotRequest)) iaas.A func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -127,53 +132,14 @@ func TestParseInput(t *testing.T) { isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { model.Name = nil - model.Labels = make(map[string]string) + model.Labels = make(map[string]any) }), }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateRequiredFlags() - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating flags: %v", err) - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -197,7 +163,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/snapshot/delete/delete.go b/internal/cmd/volume/snapshot/delete/delete.go index 0a4c17faa..7c8e06535 100644 --- a/internal/cmd/volume/snapshot/delete/delete.go +++ b/internal/cmd/volume/snapshot/delete/delete.go @@ -4,8 +4,10 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,8 +18,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + wait "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" ) const ( @@ -29,7 +31,7 @@ type inputModel struct { SnapshotId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("delete %s", snapshotIdArg), Short: "Deletes a snapshot", @@ -54,18 +56,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Get snapshot name for label - snapshotLabel, err := iaasUtils.GetSnapshotName(ctx, apiClient, model.ProjectId, model.SnapshotId) + snapshotLabel, err := iaasUtils.GetSnapshotName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SnapshotId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get snapshot name: %v", err) snapshotLabel = model.SnapshotId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q? (This cannot be undone)", snapshotLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to delete snapshot %q? (This cannot be undone)", snapshotLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -77,13 +77,13 @@ func NewCmd(params *params.CmdParams) *cobra.Command { // Wait for async operation, if async mode not enabled if !model.Async { - s := spinner.New(params.Printer) - s.Start("Deleting snapshot") - _, err = wait.DeleteSnapshotWaitHandler(ctx, apiClient, model.ProjectId, model.SnapshotId).WaitWithContext(ctx) + err := spinner.Run(params.Printer, "Deleting snapshot", func() error { + _, err = wait.DeleteSnapshotWaitHandler(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SnapshotId).WaitWithContext(ctx) + return err + }) if err != nil { return fmt.Errorf("wait for snapshot deletion: %w", err) } - s.Stop() } operationState := "Deleted" @@ -110,18 +110,10 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu SnapshotId: snapshotId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiDeleteSnapshotRequest { - return apiClient.DeleteSnapshot(ctx, model.ProjectId, model.SnapshotId) + return apiClient.DefaultAPI.DeleteSnapshot(ctx, model.ProjectId, model.Region, model.SnapshotId) } diff --git a/internal/cmd/volume/snapshot/delete/delete_test.go b/internal/cmd/volume/snapshot/delete/delete_test.go index bf1f87d15..f1b7d8d89 100644 --- a/internal/cmd/volume/snapshot/delete/delete_test.go +++ b/internal/cmd/volume/snapshot/delete/delete_test.go @@ -4,21 +4,24 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testSnapshotId = uuid.NewString() ) @@ -36,6 +39,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -47,6 +51,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, SnapshotId: testSnapshotId, @@ -58,7 +63,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiDeleteSnapshotRequest)) iaas.ApiDeleteSnapshotRequest { - request := testClient.DeleteSnapshot(testCtx, testProjectId, testSnapshotId) + request := testClient.DefaultAPI.DeleteSnapshot(testCtx, testProjectId, testRegion, testSnapshotId) for _, mod := range mods { mod(&request) } @@ -124,46 +129,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -187,7 +153,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/snapshot/describe/describe.go b/internal/cmd/volume/snapshot/describe/describe.go index 7ae36212e..8067eaad1 100644 --- a/internal/cmd/volume/snapshot/describe/describe.go +++ b/internal/cmd/volume/snapshot/describe/describe.go @@ -2,13 +2,13 @@ package describe import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -30,7 +30,7 @@ type inputModel struct { SnapshotId string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("describe %s", snapshotIdArg), Short: "Describes a snapshot", @@ -83,20 +83,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu SnapshotId: snapshotId, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiGetSnapshotRequest { - return apiClient.GetSnapshot(ctx, model.ProjectId, model.SnapshotId) + return apiClient.DefaultAPI.GetSnapshot(ctx, model.ProjectId, model.Region, model.SnapshotId) } func outputResult(p *print.Printer, outputFormat string, snapshot *iaas.Snapshot) error { @@ -104,24 +96,7 @@ func outputResult(p *print.Printer, outputFormat string, snapshot *iaas.Snapshot return fmt.Errorf("get snapshot response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(snapshot, "", " ") - if err != nil { - return fmt.Errorf("marshal snapshot: %w", err) - } - p.Outputln(string(details)) - return nil - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(snapshot, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal snapshot: %w", err) - } - p.Outputln(string(details)) - return nil - - default: + return p.OutputResult(outputFormat, snapshot, func() error { table := tables.NewTable() table.AddRow("ID", utils.PtrString(snapshot.Id)) table.AddSeparator() @@ -131,12 +106,12 @@ func outputResult(p *print.Printer, outputFormat string, snapshot *iaas.Snapshot table.AddSeparator() table.AddRow("STATUS", utils.PtrString(snapshot.Status)) table.AddSeparator() - table.AddRow("VOLUME ID", utils.PtrString(snapshot.VolumeId)) + table.AddRow("VOLUME ID", snapshot.VolumeId) table.AddSeparator() - if snapshot.Labels != nil && len(*snapshot.Labels) > 0 { + if len(snapshot.Labels) > 0 { labels := []string{} - for key, value := range *snapshot.Labels { + for key, value := range snapshot.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } table.AddRow("LABELS", strings.Join(labels, "\n")) @@ -153,5 +128,5 @@ func outputResult(p *print.Printer, outputFormat string, snapshot *iaas.Snapshot } return nil - } + }) } diff --git a/internal/cmd/volume/snapshot/describe/describe_test.go b/internal/cmd/volume/snapshot/describe/describe_test.go index 5d501758f..354d999f9 100644 --- a/internal/cmd/volume/snapshot/describe/describe_test.go +++ b/internal/cmd/volume/snapshot/describe/describe_test.go @@ -4,22 +4,26 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testSnapshotId = uuid.NewString() ) @@ -37,6 +41,7 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, } for _, mod := range mods { mod(flagValues) @@ -48,6 +53,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, SnapshotId: testSnapshotId, @@ -59,7 +65,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiGetSnapshotRequest)) iaas.ApiGetSnapshotRequest { - request := testClient.GetSnapshot(testCtx, testProjectId, testSnapshotId) + request := testClient.DefaultAPI.GetSnapshot(testCtx, testProjectId, testRegion, testSnapshotId) for _, mod := range mods { mod(&request) } @@ -125,46 +131,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -188,7 +155,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -230,11 +197,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.snapshot); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.snapshot); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/snapshot/list/list.go b/internal/cmd/volume/snapshot/list/list.go index 83b59987c..25b62470a 100644 --- a/internal/cmd/volume/snapshot/list/list.go +++ b/internal/cmd/volume/snapshot/list/list.go @@ -2,13 +2,13 @@ package list import ( "context" - "encoding/json" "fmt" "strings" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -19,8 +19,9 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -34,7 +35,7 @@ type inputModel struct { LabelSelector *string } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "Lists all snapshots", @@ -51,9 +52,9 @@ func NewCmd(params *params.CmdParams) *cobra.Command { `List snapshots filtered by label`, "$ stackit volume snapshot list --label-selector key1=value1"), ), - RunE: func(cmd *cobra.Command, _ []string) error { + RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() - model, err := parseInput(params.Printer, cmd) + model, err := parseInput(params.Printer, cmd, args) if err != nil { return err } @@ -71,25 +72,20 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return fmt.Errorf("list snapshots: %w", err) } - // Check if response is empty - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No snapshots found for project %q\n", projectLabel) - return nil - } + snapshots := resp.GetItems() - snapshots := *resp.Items + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } - // Apply limit if specified + // Truncate output if model.Limit != nil && int(*model.Limit) < len(snapshots) { snapshots = snapshots[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, snapshots) + return outputResult(params.Printer, model.OutputFormat, projectLabel, snapshots) }, } @@ -102,7 +98,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter snapshots by labels") } -func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { globalFlags := globalflags.Parse(p, cmd) if globalFlags.ProjectId == "" { return nil, &errors.ProjectIdError{} @@ -124,49 +120,24 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { LabelSelector: labelSelector, } - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } - } - + p.DebugInputModel(model) return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListSnapshotsRequest { - req := apiClient.ListSnapshots(ctx, model.ProjectId) +func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListSnapshotsInProjectRequest { + req := apiClient.DefaultAPI.ListSnapshotsInProject(ctx, model.ProjectId, model.Region) if model.LabelSelector != nil { req = req.LabelSelector(*model.LabelSelector) } return req } -func outputResult(p *print.Printer, outputFormat string, snapshots []iaas.Snapshot) error { - if snapshots == nil { - return fmt.Errorf("list snapshots response is empty") - } - - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(snapshots, "", " ") - if err != nil { - return fmt.Errorf("marshal snapshots: %w", err) +func outputResult(p *print.Printer, outputFormat, projectLabel string, snapshots []iaas.Snapshot) error { + return p.OutputResult(outputFormat, snapshots, func() error { + if len(snapshots) == 0 { + p.Outputf("No snapshots found for project %q\n", projectLabel) + return nil } - p.Outputln(string(details)) - return nil - - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(snapshots, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal snapshots: %w", err) - } - p.Outputln(string(details)) - return nil - - default: table := tables.NewTable() table.SetHeader("ID", "NAME", "SIZE", "STATUS", "VOLUME ID", "LABELS", "CREATED AT", "UPDATED AT") @@ -174,7 +145,7 @@ func outputResult(p *print.Printer, outputFormat string, snapshots []iaas.Snapsh var labelsString string if snapshot.Labels != nil { var labels []string - for key, value := range *snapshot.Labels { + for key, value := range snapshot.Labels { labels = append(labels, fmt.Sprintf("%s: %s", key, value)) } labelsString = strings.Join(labels, "\n") @@ -184,7 +155,7 @@ func outputResult(p *print.Printer, outputFormat string, snapshots []iaas.Snapsh utils.PtrString(snapshot.Name), utils.PtrGigaByteSizeDefault(snapshot.Size, "n/a"), utils.PtrString(snapshot.Status), - utils.PtrString(snapshot.VolumeId), + snapshot.VolumeId, labelsString, utils.ConvertTimePToDateTimeString(snapshot.CreatedAt), utils.ConvertTimePToDateTimeString(snapshot.UpdatedAt), @@ -194,5 +165,5 @@ func outputResult(p *print.Printer, outputFormat string, snapshots []iaas.Snapsh p.Outputln(table.Render()) return nil - } + }) } diff --git a/internal/cmd/volume/snapshot/list/list_test.go b/internal/cmd/volume/snapshot/list/list_test.go index e1f68f1b1..a579d0107 100644 --- a/internal/cmd/volume/snapshot/list/list_test.go +++ b/internal/cmd/volume/snapshot/list/list_test.go @@ -4,30 +4,36 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() ) func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - limitFlag: "10", - labelSelectorFlag: "key1=value1", + globalflags.RegionFlag: testRegion, + + limitFlag: "10", + labelSelectorFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -39,6 +45,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Limit: utils.Ptr(int64(10)), @@ -50,8 +57,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { return model } -func fixtureRequest(mods ...func(request *iaas.ApiListSnapshotsRequest)) iaas.ApiListSnapshotsRequest { - request := testClient.ListSnapshots(testCtx, testProjectId) +func fixtureRequest(mods ...func(request *iaas.ApiListSnapshotsInProjectRequest)) iaas.ApiListSnapshotsInProjectRequest { + request := testClient.DefaultAPI.ListSnapshotsInProject(testCtx, testProjectId, testRegion) request = request.LabelSelector("key1=value1") for _, mod := range mods { mod(&request) @@ -62,6 +69,7 @@ func fixtureRequest(mods ...func(request *iaas.ApiListSnapshotsRequest)) iaas.Ap func TestParseInput(t *testing.T) { tests := []struct { description string + argValues []string flagValues map[string]string isValid bool expectedModel *inputModel @@ -128,38 +136,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - model, err := parseInput(p, cmd) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -168,7 +145,7 @@ func TestBuildRequest(t *testing.T) { tests := []struct { description string model *inputModel - expectedRequest iaas.ApiListSnapshotsRequest + expectedRequest iaas.ApiListSnapshotsInProjectRequest }{ { description: "base", @@ -180,8 +157,8 @@ func TestBuildRequest(t *testing.T) { model: fixtureInputModel(func(model *inputModel) { model.LabelSelector = nil }), - expectedRequest: fixtureRequest(func(request *iaas.ApiListSnapshotsRequest) { - *request = testClient.ListSnapshots(testCtx, testProjectId) + expectedRequest: fixtureRequest(func(request *iaas.ApiListSnapshotsInProjectRequest) { + *request = testClient.DefaultAPI.ListSnapshotsInProject(testCtx, testProjectId, testRegion) }), }, } @@ -192,7 +169,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -204,6 +181,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string snapshots []iaas.Snapshot } tests := []struct { @@ -214,7 +192,7 @@ func TestOutputResult(t *testing.T) { { name: "empty", args: args{}, - wantErr: true, + wantErr: false, }, { name: "empty snapshot in slice", @@ -238,11 +216,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.snapshots); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.projectLabel, tt.args.snapshots); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/snapshot/snapshot.go b/internal/cmd/volume/snapshot/snapshot.go index 9656b1465..09640dc9b 100644 --- a/internal/cmd/volume/snapshot/snapshot.go +++ b/internal/cmd/volume/snapshot/snapshot.go @@ -2,17 +2,18 @@ package snapshot import ( "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/cmd/volume/snapshot/create" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/snapshot/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/snapshot/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/snapshot/list" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/snapshot/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "snapshot", Short: "Provides functionality for snapshots", @@ -24,7 +25,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/cmd/volume/snapshot/update/update.go b/internal/cmd/volume/snapshot/update/update.go index 543c484d1..91a91e168 100644 --- a/internal/cmd/volume/snapshot/update/update.go +++ b/internal/cmd/volume/snapshot/update/update.go @@ -4,8 +4,10 @@ import ( "context" "fmt" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -16,7 +18,7 @@ import ( iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) const ( @@ -29,10 +31,10 @@ type inputModel struct { *globalflags.GlobalFlagModel SnapshotId string Name *string - Labels map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", snapshotIdArg), Short: "Updates a snapshot", @@ -60,18 +62,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { } // Get snapshot name for label - snapshotLabel, err := iaasUtils.GetSnapshotName(ctx, apiClient, model.ProjectId, model.SnapshotId) + snapshotLabel, err := iaasUtils.GetSnapshotName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.SnapshotId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get snapshot name: %v", err) snapshotLabel = model.SnapshotId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update snapshot %q?", snapshotLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update snapshot %q?", snapshotLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -104,12 +104,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu } name := flags.FlagToStringPointer(p, cmd, nameFlag) - labels := flags.FlagToStringToStringPointer(p, cmd, labelsFlag) + labels := flags.FlagToStringToAny(p, cmd, labelsFlag) if labels == nil { - labels = &map[string]string{} + labels = map[string]any{} } - if name == nil && len(*labels) == 0 { + if name == nil && len(labels) == 0 { return nil, fmt.Errorf("either name or labels must be provided") } @@ -117,26 +117,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu GlobalFlagModel: globalFlags, SnapshotId: snapshotId, Name: name, - Labels: *labels, - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: labels, } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateSnapshotRequest { - req := apiClient.UpdateSnapshot(ctx, model.ProjectId, model.SnapshotId) + req := apiClient.DefaultAPI.UpdateSnapshot(ctx, model.ProjectId, model.Region, model.SnapshotId) payload := iaas.NewUpdateSnapshotPayloadWithDefaults() payload.Name = model.Name - payload.Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(model.Labels)) + payload.Labels = model.Labels req = req.UpdateSnapshotPayload(*payload) return req diff --git a/internal/cmd/volume/snapshot/update/update_test.go b/internal/cmd/volume/snapshot/update/update_test.go index cb4af61fc..9c61d2e4d 100644 --- a/internal/cmd/volume/snapshot/update/update_test.go +++ b/internal/cmd/volume/snapshot/update/update_test.go @@ -4,26 +4,29 @@ import ( "context" "testing" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ( + testRegion = "eu01" + testName = "test-snapshot" ) type testCtxKey struct{} var ( testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") - testClient = &iaas.APIClient{} + testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} testProjectId = uuid.NewString() testSnapshotId = uuid.NewString() - testName = "test-snapshot" - testLabels = map[string]string{"key1": "value1"} + testLabels = map[string]any{"key1": "value1"} ) func fixtureArgValues(mods ...func(argValues []string)) []string { @@ -39,8 +42,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ globalflags.ProjectIdFlag: testProjectId, - nameFlag: testName, - labelsFlag: "key1=value1", + globalflags.RegionFlag: testRegion, + + nameFlag: testName, + labelsFlag: "key1=value1", } for _, mod := range mods { mod(flagValues) @@ -52,10 +57,11 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, SnapshotId: testSnapshotId, - Name: &testName, + Name: utils.Ptr(testName), Labels: testLabels, } for _, mod := range mods { @@ -65,10 +71,10 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateSnapshotRequest)) iaas.ApiUpdateSnapshotRequest { - request := testClient.UpdateSnapshot(testCtx, testProjectId, testSnapshotId) + request := testClient.DefaultAPI.UpdateSnapshot(testCtx, testProjectId, testRegion, testSnapshotId) payload := iaas.NewUpdateSnapshotPayloadWithDefaults() - payload.Name = &testName - payload.Labels = utils.ConvertStringMapToInterfaceMap(utils.Ptr(testLabels)) + payload.Name = utils.Ptr(testName) + payload.Labels = testLabels request = request.UpdateSnapshotPayload(*payload) for _, mod := range mods { @@ -149,7 +155,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = make(map[string]string) + model.Labels = make(map[string]any) }), }, { @@ -167,46 +173,7 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) - err := globalflags.Configure(cmd.Flags()) - if err != nil { - t.Fatalf("configure global flags: %v", err) - } - - for flag, value := range tt.flagValues { - err := cmd.Flags().Set(flag, value) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("setting flag --%s=%s: %v", flag, value, err) - } - } - - err = cmd.ValidateArgs(tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error validating args: %v", err) - } - - model, err := parseInput(p, cmd, tt.argValues) - if err != nil { - if !tt.isValid { - return - } - t.Fatalf("error parsing input: %v", err) - } - - if !tt.isValid { - t.Fatalf("did not fail on invalid input") - } - diff := cmp.Diff(model, tt.expectedModel) - if diff != "" { - t.Fatalf("Data does not match: %s", diff) - } + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) }) } } @@ -230,7 +197,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/internal/cmd/volume/update/update.go b/internal/cmd/volume/update/update.go index 263f333af..f19b1cb20 100644 --- a/internal/cmd/volume/update/update.go +++ b/internal/cmd/volume/update/update.go @@ -2,13 +2,13 @@ package update import ( "context" - "encoding/json" "fmt" - "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -18,7 +18,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" iaasUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) const ( @@ -34,10 +33,10 @@ type inputModel struct { VolumeId string Name *string Description *string - Labels *map[string]string + Labels map[string]any } -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("update %s", volumeIdArg), Short: "Updates a volume", @@ -70,18 +69,16 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return err } - volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient, model.ProjectId, model.VolumeId) + volumeLabel, err := iaasUtils.GetVolumeName(ctx, apiClient.DefaultAPI, model.ProjectId, model.Region, model.VolumeId) if err != nil { params.Printer.Debug(print.ErrorLevel, "get volume name: %v", err) volumeLabel = model.VolumeId } - if !model.AssumeYes { - prompt := fmt.Sprintf("Are you sure you want to update volume %q?", volumeLabel) - err = params.Printer.PromptForConfirmation(prompt) - if err != nil { - return err - } + prompt := fmt.Sprintf("Are you sure you want to update volume %q?", volumeLabel) + err = params.Printer.PromptForConfirmation(prompt) + if err != nil { + return err } // Call API @@ -117,28 +114,20 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu Name: flags.FlagToStringPointer(p, cmd, nameFlag), VolumeId: volumeId, Description: flags.FlagToStringPointer(p, cmd, descriptionFlag), - Labels: flags.FlagToStringToStringPointer(p, cmd, labelFlag), - } - - if p.IsVerbosityDebug() { - modelStr, err := print.BuildDebugStrFromInputModel(model) - if err != nil { - p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) - } else { - p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) - } + Labels: flags.FlagToStringToAny(p, cmd, labelFlag), } + p.DebugInputModel(model) return &model, nil } func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiUpdateVolumeRequest { - req := apiClient.UpdateVolume(ctx, model.ProjectId, model.VolumeId) + req := apiClient.DefaultAPI.UpdateVolume(ctx, model.ProjectId, model.Region, model.VolumeId) payload := iaas.UpdateVolumePayload{ Name: model.Name, Description: model.Description, - Labels: utils.ConvertStringMapToInterfaceMap(model.Labels), + Labels: model.Labels, } return req.UpdateVolumePayload(payload) @@ -148,25 +137,8 @@ func outputResult(p *print.Printer, outputFormat, volumeLabel string, volume *ia if volume == nil { return fmt.Errorf("volume response is empty") } - switch outputFormat { - case print.JSONOutputFormat: - details, err := json.MarshalIndent(volume, "", " ") - if err != nil { - return fmt.Errorf("marshal volume: %w", err) - } - p.Outputln(string(details)) - - return nil - case print.YAMLOutputFormat: - details, err := yaml.MarshalWithOptions(volume, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) - if err != nil { - return fmt.Errorf("marshal volume: %w", err) - } - p.Outputln(string(details)) - - return nil - default: + return p.OutputResult(outputFormat, volume, func() error { p.Outputf("Updated volume %q.\n", volumeLabel) return nil - } + }) } diff --git a/internal/cmd/volume/update/update_test.go b/internal/cmd/volume/update/update_test.go index 1628bf26a..8700f35f0 100644 --- a/internal/cmd/volume/update/update_test.go +++ b/internal/cmd/volume/update/update_test.go @@ -7,19 +7,22 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/cmd/params" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var projectIdFlag = globalflags.ProjectIdFlag +const ( + testRegion = "eu01" +) type testCtxKey struct{} var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") -var testClient = &iaas.APIClient{} +var testClient = &iaas.APIClient{DefaultAPI: &iaas.DefaultAPIService{}} var testProjectId = uuid.NewString() var testVolumeId = uuid.NewString() @@ -36,8 +39,10 @@ func fixtureArgValues(mods ...func(argValues []string)) []string { func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ + globalflags.ProjectIdFlag: testProjectId, + globalflags.RegionFlag: testRegion, + nameFlag: "example-volume-name", - projectIdFlag: testProjectId, descriptionFlag: "example-volume-desc", labelFlag: "key=value", } @@ -51,14 +56,15 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ GlobalFlagModel: &globalflags.GlobalFlagModel{ ProjectId: testProjectId, + Region: testRegion, Verbosity: globalflags.VerbosityDefault, }, Name: utils.Ptr("example-volume-name"), Description: utils.Ptr("example-volume-desc"), VolumeId: testVolumeId, - Labels: utils.Ptr(map[string]string{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(model) @@ -67,7 +73,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { } func fixtureRequest(mods ...func(request *iaas.ApiUpdateVolumeRequest)) iaas.ApiUpdateVolumeRequest { - request := testClient.UpdateVolume(testCtx, testProjectId, testVolumeId) + request := testClient.DefaultAPI.UpdateVolume(testCtx, testProjectId, testRegion, testVolumeId) request = request.UpdateVolumePayload(fixturePayload()) for _, mod := range mods { mod(&request) @@ -79,9 +85,9 @@ func fixturePayload(mods ...func(payload *iaas.UpdateVolumePayload)) iaas.Update payload := iaas.UpdateVolumePayload{ Name: utils.Ptr("example-volume-name"), Description: utils.Ptr("example-volume-desc"), - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]any{ "key": "value", - }), + }, } for _, mod := range mods { mod(&payload) @@ -114,7 +120,7 @@ func TestParseInput(t *testing.T) { description: "project id missing", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - delete(flagValues, projectIdFlag) + delete(flagValues, globalflags.ProjectIdFlag) }), isValid: false, }, @@ -122,7 +128,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 1", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "" + flagValues[globalflags.ProjectIdFlag] = "" }), isValid: false, }, @@ -130,7 +136,7 @@ func TestParseInput(t *testing.T) { description: "project id invalid 2", argValues: fixtureArgValues(), flagValues: fixtureFlagValues(func(flagValues map[string]string) { - flagValues[projectIdFlag] = "invalid-uuid" + flagValues[globalflags.ProjectIdFlag] = "invalid-uuid" }), isValid: false, }, @@ -167,7 +173,7 @@ func TestParseInput(t *testing.T) { }), isValid: true, expectedModel: fixtureInputModel(func(model *inputModel) { - model.Labels = &map[string]string{ + model.Labels = map[string]any{ "key": "value", } }), @@ -176,8 +182,8 @@ func TestParseInput(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - cmd := NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() + cmd := NewCmd(params.CmdParams) err := globalflags.Configure(cmd.Flags()) if err != nil { t.Fatalf("configure global flags: %v", err) @@ -209,7 +215,7 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating args: %v", err) } - model, err := parseInput(p, cmd, tt.argValues) + model, err := parseInput(params.Printer, cmd, tt.argValues) if err != nil { if !tt.isValid { return @@ -247,7 +253,7 @@ func TestBuildRequest(t *testing.T) { diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), - cmpopts.EquateComparable(testCtx), + cmpopts.EquateComparable(testCtx, iaas.DefaultAPIService{}), ) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -280,11 +286,10 @@ func TestOutputResult(t *testing.T) { wantErr: false, }, } - p := print.NewPrinter() - p.Cmd = NewCmd(¶ms.CmdParams{Printer: p}) + params := testparams.NewTestParams() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := outputResult(p, tt.args.outputFormat, tt.args.volumeLabel, tt.args.volume); (err != nil) != tt.wantErr { + if err := outputResult(params.Printer, tt.args.outputFormat, tt.args.volumeLabel, tt.args.volume); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/internal/cmd/volume/volume.go b/internal/cmd/volume/volume.go index 8da9cbd13..a6967a9ae 100644 --- a/internal/cmd/volume/volume.go +++ b/internal/cmd/volume/volume.go @@ -1,7 +1,6 @@ package volume import ( - "github.com/stackitcloud/stackit-cli/internal/cmd/params" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/backup" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/create" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/delete" @@ -12,12 +11,13 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/volume/snapshot" "github.com/stackitcloud/stackit-cli/internal/cmd/volume/update" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/cobra" ) -func NewCmd(params *params.CmdParams) *cobra.Command { +func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "volume", Short: "Provides functionality for volumes", @@ -29,7 +29,7 @@ func NewCmd(params *params.CmdParams) *cobra.Command { return cmd } -func addSubcommands(cmd *cobra.Command, params *params.CmdParams) { +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { cmd.AddCommand(create.NewCmd(params)) cmd.AddCommand(delete.NewCmd(params)) cmd.AddCommand(describe.NewCmd(params)) diff --git a/internal/pkg/auth/auth.go b/internal/pkg/auth/auth.go index 9a9b48817..3d241ed9a 100644 --- a/internal/pkg/auth/auth.go +++ b/internal/pkg/auth/auth.go @@ -12,6 +12,7 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/spf13/viper" + sdkAuth "github.com/stackitcloud/stackit-sdk-go/core/auth" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" ) @@ -25,7 +26,7 @@ type tokenClaims struct { // // If the user was logged in and the user session expired, reauthorizeUserRoutine is called to reauthenticate the user again. // If the environment variable STACKIT_ACCESS_TOKEN is set this token is used instead. -func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print.Printer, _ bool) error) (authCfgOption sdkConfig.ConfigurationOption, err error) { +func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print.Printer, _ UserAuthConfig) error) (authCfgOption sdkConfig.ConfigurationOption, err error) { // Get access token from env and use this if present accessToken := os.Getenv(envAccessTokenName) if accessToken != "" { @@ -33,6 +34,38 @@ func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print return authCfgOption, nil } + // use workload identity federation (OIDC) if enabled; takes priority over stored flows + if IsOIDCEnabled() { + p.Debug(print.DebugLevel, "authenticating using workload identity federation (OIDC)") + + email := OIDCServiceAccountEmail() + if email == "" { + return nil, fmt.Errorf( + "env var %s must be set when %s is enabled", + EnvServiceAccountEmail, EnvUseOIDC, + ) + } + + tokenFunc, err := OIDCTokenFunc() + if err != nil { + return nil, err + } + + wifCfg := &sdkConfig.Configuration{ + WorkloadIdentityFederation: true, + ServiceAccountEmail: email, + ServiceAccountFederatedTokenFunc: tokenFunc, + TokenCustomUrl: viper.GetString(config.TokenCustomEndpointKey), + } + + rt, err := sdkAuth.WorkloadIdentityFederationAuth(wifCfg) + if err != nil { + return nil, fmt.Errorf("initialize workload identity federation: %w", err) + } + + return sdkConfig.WithCustomAuth(rt), nil + } + flow, err := GetAuthFlow() if err != nil { return nil, fmt.Errorf("get authentication flow: %w", err) @@ -70,7 +103,10 @@ func AuthenticationConfig(p *print.Printer, reauthorizeUserRoutine func(p *print case AUTH_FLOW_USER_TOKEN: p.Debug(print.DebugLevel, "authenticating using user token") if userSessionExpired { - err = reauthorizeUserRoutine(p, true) + err = reauthorizeUserRoutine(p, UserAuthConfig{ + IsReauthentication: true, + Port: nil, + }) if err != nil { return nil, fmt.Errorf("user login: %w", err) } @@ -110,15 +146,23 @@ func GetAccessToken() (string, error) { func getStartingSessionExpiresAtUnix() (string, error) { sessionStart := time.Now() - sessionTimeLimitString := viper.GetString(config.SessionTimeLimitKey) - sessionTimeLimit, err := time.ParseDuration(sessionTimeLimitString) + sessionTimeLimit, err := getSessionExpiration() if err != nil { - return "", fmt.Errorf("parse session time limit \"%s\": %w", sessionTimeLimitString, err) + return "", err } sessionExpiresAt := sessionStart.Add(sessionTimeLimit) return strconv.FormatInt(sessionExpiresAt.Unix(), 10), nil } +func getSessionExpiration() (time.Duration, error) { + sessionTimeLimitString := viper.GetString(config.SessionTimeLimitKey) + duration, err := time.ParseDuration(sessionTimeLimitString) + if err != nil { + return 0, fmt.Errorf("parse session time limit \"%s\": %w", sessionTimeLimitString, err) + } + return duration, nil +} + func getEmailFromToken(token string) (string, error) { // We can safely use ParseUnverified because we are not authenticating the user at this point, // We are parsing the token just to get the service account e-mail @@ -202,9 +246,24 @@ func GetValidAccessToken(p *print.Printer) (string, error) { // Refresh the tokens err = refreshTokens(utf) if err != nil { - return "", fmt.Errorf("refresh access token: %w", err) + return "", fmt.Errorf("access token and refresh token expired: %w", err) } // Return the new access token return utf.accessToken, nil } + +// EnsureIDPTokenEndpoint ensures that the `IDP_TOKEN_ENDPOINT` auth field is set. +// This field is by default only initialized for user accounts. Call this method to also +// initialize it for service accounts. +func EnsureIDPTokenEndpoint(p *print.Printer) error { + idpTokenEndpoint, err := GetAuthField(IDP_TOKEN_ENDPOINT) + if err != nil { + return fmt.Errorf("failed to check idp token endpoint configuration value: %w", err) + } + if idpTokenEndpoint == "" { + _, err := retrieveIDPWellKnownConfig(p) + return err + } + return nil +} diff --git a/internal/pkg/auth/auth_test.go b/internal/pkg/auth/auth_test.go index f7355f365..192645d21 100644 --- a/internal/pkg/auth/auth_test.go +++ b/internal/pkg/auth/auth_test.go @@ -4,9 +4,11 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" - "io" + "net/http" + "net/http/httptest" "strconv" "testing" "time" @@ -14,11 +16,13 @@ import ( "github.com/golang-jwt/jwt/v5" "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-sdk-go/core/clients" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/zalando/go-keyring" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) const saKeyStrPattern = `{ @@ -151,6 +155,7 @@ func TestAuthenticationConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { + t.Setenv(EnvUseOIDC, "") // ensure OIDC mode is off for these tests keyring.MockInit() timestamp := time.Now().Add(24 * time.Hour) authFields := make(map[authFieldKey]string) @@ -188,7 +193,7 @@ func TestAuthenticationConfig(t *testing.T) { } reauthorizeUserCalled := false - reauthenticateUser := func(_ *print.Printer, _ bool) error { + reauthenticateUser := func(_ *print.Printer, _ UserAuthConfig) error { if reauthorizeUserCalled { t.Errorf("user reauthorized more than once") } @@ -196,11 +201,9 @@ func TestAuthenticationConfig(t *testing.T) { return nil } - cmd := &cobra.Command{} - cmd.SetOut(io.Discard) // Suppresses console prints - p := &print.Printer{Cmd: cmd} + params := testparams.NewTestParams() - authCfgOption, err := AuthenticationConfig(p, reauthenticateUser) + authCfgOption, err := AuthenticationConfig(params.Printer, reauthenticateUser) if !tt.isValid { if err == nil { @@ -235,58 +238,28 @@ func TestAuthenticationConfig(t *testing.T) { func TestInitKeyFlow(t *testing.T) { tests := []struct { - description string - accessTokenSet bool - refreshToken string - saKey string - privateKeySet bool - tokenEndpoint string - isValid bool + description string + saKey string + privateKeySet bool + isValid bool }{ { - description: "base", - accessTokenSet: true, - refreshToken: "refresh_token", - saKey: testServiceAccountKey, - privateKeySet: true, - tokenEndpoint: "token_url", - isValid: true, - }, - { - description: "invalid_service_account_key", - accessTokenSet: true, - refreshToken: "refresh_token", - saKey: "", - privateKeySet: true, - tokenEndpoint: "token_url", - isValid: false, - }, - { - description: "invalid_private_key", - accessTokenSet: true, - refreshToken: "refresh_token", - saKey: testServiceAccountKey, - privateKeySet: false, - tokenEndpoint: "token_url", - isValid: false, + description: "base", + saKey: testServiceAccountKey, + privateKeySet: true, + isValid: true, }, { - description: "invalid_access_token", - accessTokenSet: false, - refreshToken: "refresh_token", - saKey: testServiceAccountKey, - privateKeySet: true, - tokenEndpoint: "token_url", - isValid: false, + description: "invalid_service_account_key", + saKey: "", + privateKeySet: true, + isValid: false, }, { - description: "empty_refresh_token", - accessTokenSet: false, - refreshToken: "", - saKey: testServiceAccountKey, - privateKeySet: true, - tokenEndpoint: "token_url", - isValid: false, + description: "no_private_key_set", + saKey: testServiceAccountKey, + privateKeySet: false, + isValid: false, }, } @@ -297,13 +270,11 @@ func TestInitKeyFlow(t *testing.T) { authFields := make(map[authFieldKey]string) var accessToken string var err error - if tt.accessTokenSet { - accessTokenJWT := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(timestamp)}) - accessToken, err = accessTokenJWT.SignedString(testSigningKey) - if err != nil { - t.Fatalf("Get test access token as string: %s", err) - } + accessTokenJWT := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(timestamp)}) + accessToken, err = accessTokenJWT.SignedString(testSigningKey) + if err != nil { + t.Fatalf("Get test access token as string: %s", err) } if tt.privateKeySet { privateKey, err := generatePrivateKey() @@ -313,16 +284,42 @@ func TestInitKeyFlow(t *testing.T) { authFields[PRIVATE_KEY] = string(privateKey) } authFields[ACCESS_TOKEN] = accessToken - authFields[REFRESH_TOKEN] = tt.refreshToken authFields[SERVICE_ACCOUNT_KEY] = tt.saKey - authFields[TOKEN_CUSTOM_ENDPOINT] = tt.tokenEndpoint + + // Mock server to avoid HTTP calls + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + resp := clients.TokenResponseBody{ + AccessToken: accessToken, + ExpiresIn: 3600, + TokenType: "Bearer", + } + jsonResp, err := json.Marshal(resp) //nolint:gosec // not a real access token, just a testcase + if err != nil { + t.Fatalf("Failed to marshal json: %v", err) + } + _, err = w.Write(jsonResp) + if err != nil { + t.Fatalf("Failed to write response: %v", err) + } + })) + defer server.Close() + authFields[TOKEN_CUSTOM_ENDPOINT] = server.URL + err = SetAuthFieldMap(authFields) if err != nil { t.Fatalf("Failed to set in auth storage: %v", err) } keyFlowWithStorage, err := initKeyFlowWithStorage() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("Expected no error but error was returned: %v", err) + } + getAccessToken, err := keyFlowWithStorage.keyFlow.GetAccessToken() if !tt.isValid { if err == nil { t.Fatalf("Expected error but no error was returned") @@ -331,15 +328,8 @@ func TestInitKeyFlow(t *testing.T) { if err != nil { t.Fatalf("Expected no error but error was returned: %v", err) } - expectedToken := &clients.TokenResponseBody{ - AccessToken: accessToken, - ExpiresIn: int(timestamp.Unix()), - RefreshToken: tt.refreshToken, - Scope: "", - TokenType: "Bearer", - } - if !cmp.Equal(*expectedToken, keyFlowWithStorage.keyFlow.GetToken()) { - t.Errorf("The returned result is wrong. Expected %+v, got %+v", expectedToken, keyFlowWithStorage.keyFlow.GetToken()) + if !cmp.Equal(accessToken, getAccessToken) { + t.Errorf("The returned result is wrong. Expected %+v, got %+v", accessToken, getAccessToken) } } }) diff --git a/internal/pkg/auth/exchange.go b/internal/pkg/auth/exchange.go new file mode 100644 index 000000000..0c005d237 --- /dev/null +++ b/internal/pkg/auth/exchange.go @@ -0,0 +1,90 @@ +package auth + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +func ExchangeToken(ctx context.Context, idpClient *http.Client, accessToken, resource string) (string, error) { + tokenEndpoint, err := GetAuthField(IDP_TOKEN_ENDPOINT) + if err != nil { + return "", fmt.Errorf("get idp token endpoint: %w", err) + } + + req, err := buildRequestToExchangeTokens(ctx, tokenEndpoint, accessToken, resource) + if err != nil { + return "", fmt.Errorf("build request: %w", err) + } + resp, err := idpClient.Do(req) + if err != nil { + return "", fmt.Errorf("call API: %w", err) + } + defer func() { + tempErr := resp.Body.Close() + if tempErr != nil { + err = fmt.Errorf("close response body: %w", tempErr) + } + }() + + clusterToken, err := parseTokenExchangeResponse(resp) + if err != nil { + return "", fmt.Errorf("parse API response: %w", err) + } + return clusterToken, nil +} + +func buildRequestToExchangeTokens(ctx context.Context, tokenEndpoint, accessToken, resource string) (*http.Request, error) { + idpClientID, err := getIDPClientID() + if err != nil { + return nil, err + } + + form := url.Values{} + form.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + form.Set("client_id", idpClientID) + form.Set("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") + form.Set("requested_token_type", "urn:ietf:params:oauth:token-type:id_token") + form.Set("scope", "openid profile email groups") + form.Set("subject_token", accessToken) + form.Set("resource", resource) + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + tokenEndpoint, + strings.NewReader(form.Encode()), + ) + if err != nil { + return nil, fmt.Errorf("build exchange request: %w", err) + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + return req, nil +} + +func parseTokenExchangeResponse(resp *http.Response) (accessToken string, err error) { + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read body: %w", err) + } + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("non-OK %d status: %s", resp.StatusCode, string(respBody)) + } + + respContent := struct { + AccessToken string `json:"access_token"` + }{} + err = json.Unmarshal(respBody, &respContent) + if err != nil { + return "", fmt.Errorf("unmarshal body: %w", err) + } + if respContent.AccessToken == "" { + return "", fmt.Errorf("no access token found") + } + return respContent.AccessToken, nil +} diff --git a/internal/pkg/auth/exchange_test.go b/internal/pkg/auth/exchange_test.go new file mode 100644 index 000000000..9ecf30cf4 --- /dev/null +++ b/internal/pkg/auth/exchange_test.go @@ -0,0 +1,187 @@ +package auth + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/http/httputil" + "net/url" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/zalando/go-keyring" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") + +var testAccessToken = "access-token-test-" + uuid.NewString() +var testExchangedToken = "access-token-exchanged-" + uuid.NewString() +var testExchangeResource = "resource://for/token/exchange" + +func fixtureTokenExchangeRequest(tokenEndpoint string) *http.Request { + form := url.Values{} + form.Set("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") + form.Set("client_id", "stackit-cli-0000-0000-000000000001") + form.Set("subject_token_type", "urn:ietf:params:oauth:token-type:access_token") + form.Set("requested_token_type", "urn:ietf:params:oauth:token-type:id_token") + form.Set("scope", "openid profile email groups") + form.Set("subject_token", testAccessToken) + form.Set("resource", testExchangeResource) + + req, _ := http.NewRequestWithContext( + testCtx, + http.MethodPost, + tokenEndpoint, + strings.NewReader(form.Encode()), + ) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + return req +} + +func fixtureTokenExchangeResponse() string { + type exchangeReponse struct { + AccessToken string `json:"access_token"` + IssuedTokeType string `json:"issued_token_type"` + TokenType string `json:"token_type"` + } + response, _ := json.Marshal(exchangeReponse{ //nolint:gosec // just a testcase, no valid credentials + AccessToken: testExchangedToken, + IssuedTokeType: "urn:ietf:params:oauth:token-type:id_token", + TokenType: "Bearer", + }) + return string(response) +} + +func TestBuildTokenExchangeRequest(t *testing.T) { + expectedRequest := fixtureTokenExchangeRequest(testTokenEndpoint) + req, err := buildRequestToExchangeTokens(testCtx, testTokenEndpoint, testAccessToken, testExchangeResource) + if err != nil { + t.Fatalf("func returned error: %s", err) + } + // directly using cmp.Diff is not possible, so dump the requests first + expected, err := httputil.DumpRequest(expectedRequest, true) + if err != nil { + t.Fatalf("fail to dump expected: %s", err) + } + actual, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatalf("fail to dump actual: %s", err) + } + diff := cmp.Diff(actual, expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } +} + +func TestParseTokenExchangeResponse(t *testing.T) { + response := fixtureTokenExchangeResponse() + + tests := []struct { + description string + response string + status int + expectError bool + }{ + { + description: "valid response", + response: response, + status: http.StatusOK, + }, + { + description: "error status", + response: response, // valid response to make sure the status code is checked + status: http.StatusForbidden, + expectError: true, + }, + { + description: "error content", + response: "{}", + status: http.StatusOK, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + w := httptest.NewRecorder() + w.WriteHeader(tt.status) + _, _ = w.WriteString(tt.response) + resp := w.Result() + + defer func() { + tempErr := resp.Body.Close() + if tempErr != nil { + t.Fatalf("failed to close response body: %v", tempErr) + } + }() + accessToken, err := parseTokenExchangeResponse(resp) + if tt.expectError { + if err == nil { + t.Fatal("expected error got nil") + } + } else { + if err != nil { + t.Fatalf("func returned error: %s", err) + } + diff := cmp.Diff(accessToken, testExchangedToken) + if diff != "" { + t.Fatalf("Token does not match: %s", diff) + } + } + }) + } +} + +func TestExchangeToken(t *testing.T) { + var request *http.Request + response := fixtureTokenExchangeResponse() + + handler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // only compare body as the headers will differ + expected, err := io.ReadAll(request.Body) + if err != nil { + t.Errorf("fail to dump expected: %s", err) + } + actual, err := io.ReadAll(req.Body) + if err != nil { + t.Errorf("fail to dump actual: %s", err) + } + diff := cmp.Diff(actual, expected) + if diff != "" { + w.WriteHeader(http.StatusBadRequest) + t.Errorf("request mismatch: %v", diff) + return + } + + w.Header().Set("Content-Type", "application/json") + _, err = w.Write([]byte(response)) + if err != nil { + t.Errorf("Failed to write response: %v", err) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + request = fixtureTokenExchangeRequest(server.URL) + // use mock keyring to inject the token endpoint URL + keyring.MockInit() + err := SetAuthField(IDP_TOKEN_ENDPOINT, server.URL) + if err != nil { + t.Errorf("failed to inject idp token endpoint: %s", err) + } + + idToken, err := ExchangeToken(testCtx, server.Client(), testAccessToken, testExchangeResource) + if err != nil { + t.Fatalf("func returned error: %s", err) + } + diff := cmp.Diff(idToken, testExchangedToken) + if diff != "" { + t.Fatalf("Exchanged token does not match: %s", diff) + } +} diff --git a/internal/pkg/auth/oidc.go b/internal/pkg/auth/oidc.go new file mode 100644 index 000000000..38584687e --- /dev/null +++ b/internal/pkg/auth/oidc.go @@ -0,0 +1,80 @@ +package auth + +import ( + "context" + "fmt" + "os" + + "github.com/stackitcloud/stackit-sdk-go/core/oidcadapters" +) + +const ( + EnvUseOIDC = "STACKIT_USE_OIDC" + EnvServiceAccountEmail = "STACKIT_SERVICE_ACCOUNT_EMAIL" + EnvServiceAccountFederatedToken = "STACKIT_SERVICE_ACCOUNT_FEDERATED_TOKEN" //nolint:gosec // linter false positive + EnvFederatedTokenFile = "STACKIT_FEDERATED_TOKEN_FILE" //nolint:gosec // linter false positive + EnvGitHubRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL" + EnvGitHubRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN" //nolint:gosec // linter false positive + EnvAzureOIDCRequestURI = "SYSTEM_OIDCREQUESTURI" + EnvAzureAccessToken = "SYSTEM_ACCESSTOKEN" //nolint:gosec // linter false positive +) + +func IsOIDCEnabled() bool { + return os.Getenv(EnvUseOIDC) == "1" +} + +// IsOIDCEnabledWithOverride resolves OIDC mode using explicit input first and env fallback. +// If useOIDC is not nil, its value is used directly; otherwise STACKIT_USE_OIDC is evaluated. +func IsOIDCEnabledWithOverride(useOIDC *bool) bool { + if useOIDC != nil { + return *useOIDC + } + + return IsOIDCEnabled() +} + +func OIDCServiceAccountEmail() string { + return os.Getenv(EnvServiceAccountEmail) +} + +// TokenFunc returns the OIDCTokenFunc to use for Workload Identity Federation. +// It checks the following token sources in order: STACKIT_SERVICE_ACCOUNT_FEDERATED_TOKEN, +// STACKIT_FEDERATED_TOKEN_FILE, GitHub Actions (ACTIONS_ID_TOKEN_REQUEST_URL + +// ACTIONS_ID_TOKEN_REQUEST_TOKEN), and Azure DevOps (SYSTEM_OIDCREQUESTURI + SYSTEM_ACCESSTOKEN). +// Returns an error if no source is detected. +func OIDCTokenFunc() (oidcadapters.OIDCTokenFunc, error) { + // static token provided directly via env var + if token := os.Getenv(EnvServiceAccountFederatedToken); token != "" { + return func(_ context.Context) (string, error) { + return token, nil + }, nil + } + + // token read from filesystem path via env var + if tokenFilePath := os.Getenv(EnvFederatedTokenFile); tokenFilePath != "" { + return oidcadapters.ReadJWTFromFileSystem(tokenFilePath), nil + } + + // GitHub Actions + if ghURL := os.Getenv(EnvGitHubRequestURL); ghURL != "" { + if ghToken := os.Getenv(EnvGitHubRequestToken); ghToken != "" { + return oidcadapters.RequestGHOIDCToken(ghURL, ghToken), nil + } + } + + // Azure DevOps + if adoURL := os.Getenv(EnvAzureOIDCRequestURI); adoURL != "" { + if adoToken := os.Getenv(EnvAzureAccessToken); adoToken != "" { + return oidcadapters.RequestAzureDevOpsOIDCToken(adoURL, adoToken, ""), nil + } + } + + return nil, fmt.Errorf( + "%s is enabled but no OIDC token source was detected\n"+ + "Provide the token via %s or %s, or run in a supported CI environment:\n"+ + " - GitHub Actions: grant 'id-token: write' permission; %s and %s are set automatically by the runner\n"+ + " - Azure DevOps: pass 'SYSTEM_ACCESSTOKEN: $(System.AccessToken)' in your pipeline step", + EnvUseOIDC, EnvServiceAccountFederatedToken, EnvFederatedTokenFile, + EnvGitHubRequestURL, EnvGitHubRequestToken, + ) +} diff --git a/internal/pkg/auth/oidc_test.go b/internal/pkg/auth/oidc_test.go new file mode 100644 index 000000000..12e762980 --- /dev/null +++ b/internal/pkg/auth/oidc_test.go @@ -0,0 +1,200 @@ +package auth_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func TestIsEnabled(t *testing.T) { + tests := []struct { + value string + expected bool + }{ + {"1", true}, + {"0", false}, + {"", false}, + {"true", false}, + {"yes", false}, + {"random", false}, + } + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + t.Setenv(auth.EnvUseOIDC, tt.value) + got := auth.IsOIDCEnabled() + if got != tt.expected { + t.Errorf("IsOIDCEnabled() = %v, want %v (env=%q)", got, tt.expected, tt.value) + } + }) + } +} + +func TestIsEnabled_Unset(t *testing.T) { + // When the env var is not set at all IsEnabled must return false + t.Setenv(auth.EnvUseOIDC, "") + if auth.IsOIDCEnabled() { + t.Error("IsOIDCEnabled() = true, want false when env var is empty") + } +} + +func TestIsOIDCEnabledWithOverride(t *testing.T) { + tests := []struct { + description string + envUseOIDC string + override *bool + expected bool + }{ + { + description: "uses env when override is nil", + envUseOIDC: "1", + override: nil, + expected: true, + }, + { + description: "override true wins over env false", + envUseOIDC: "0", + override: utils.Ptr(true), + expected: true, + }, + { + description: "override false wins over env true", + envUseOIDC: "1", + override: utils.Ptr(false), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + t.Setenv(auth.EnvUseOIDC, tt.envUseOIDC) + + got := auth.IsOIDCEnabledWithOverride(tt.override) + if got != tt.expected { + t.Errorf("IsOIDCEnabledWithOverride() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestServiceAccountEmail(t *testing.T) { + const want = "ci@sa.stackit.cloud" + t.Setenv(auth.EnvServiceAccountEmail, want) + if got := auth.OIDCServiceAccountEmail(); got != want { + t.Errorf("OIDCServiceAccountEmail() = %q, want %q", got, want) + } +} + +func TestTokenFunc_StaticToken(t *testing.T) { + const want = "my-static-oidc-token" + t.Setenv(auth.EnvServiceAccountFederatedToken, want) + // ensure GitHub / Azure vars are absent so we hit the static path first + t.Setenv(auth.EnvGitHubRequestURL, "") + t.Setenv(auth.EnvGitHubRequestToken, "") + t.Setenv(auth.EnvAzureOIDCRequestURI, "") + t.Setenv(auth.EnvAzureAccessToken, "") + t.Setenv(auth.EnvFederatedTokenFile, "") + + fn, err := auth.OIDCTokenFunc() + if err != nil { + t.Fatalf("OIDCTokenFunc() unexpected error: %v", err) + } + got, err := fn(context.Background()) + if err != nil { + t.Fatalf("fn() unexpected error: %v", err) + } + if got != want { + t.Errorf("fn() = %q, want %q", got, want) + } +} + +func TestTokenFunc_GitHubActions(t *testing.T) { + // Spin up a fake GitHub OIDC endpoint that matches the SDK's expected format. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{"value": "gh-oidc-token"}) + })) + defer srv.Close() + + t.Setenv(auth.EnvServiceAccountFederatedToken, "") + t.Setenv(auth.EnvFederatedTokenFile, "") + t.Setenv(auth.EnvGitHubRequestURL, srv.URL) + t.Setenv(auth.EnvGitHubRequestToken, "gh-bearer-token") + t.Setenv(auth.EnvAzureOIDCRequestURI, "") + t.Setenv(auth.EnvAzureAccessToken, "") + + fn, err := auth.OIDCTokenFunc() + if err != nil { + t.Fatalf("OIDCTokenFunc() unexpected error: %v", err) + } + got, err := fn(context.Background()) + if err != nil { + t.Fatalf("fn() unexpected error: %v", err) + } + if got != "gh-oidc-token" { + t.Errorf("fn() = %q, want %q", got, "gh-oidc-token") + } +} + +func TestTokenFunc_AzureDevOps(t *testing.T) { + // Spin up a fake Azure DevOps OIDC endpoint that matches the SDK's expected format. + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + tok := "ado-oidc-token" + _ = json.NewEncoder(w).Encode(map[string]*string{"oidcToken": &tok}) + })) + defer srv.Close() + + t.Setenv(auth.EnvServiceAccountFederatedToken, "") + t.Setenv(auth.EnvFederatedTokenFile, "") + t.Setenv(auth.EnvGitHubRequestURL, "") + t.Setenv(auth.EnvGitHubRequestToken, "") + t.Setenv(auth.EnvAzureOIDCRequestURI, srv.URL) + t.Setenv(auth.EnvAzureAccessToken, "ado-access-token") + + fn, err := auth.OIDCTokenFunc() + if err != nil { + t.Fatalf("OIDCTokenFunc() unexpected error: %v", err) + } + got, err := fn(context.Background()) + if err != nil { + t.Fatalf("fn() unexpected error: %v", err) + } + if got != "ado-oidc-token" { + t.Errorf("fn() = %q, want %q", got, "ado-oidc-token") + } +} + +func TestTokenFunc_NoSource(t *testing.T) { + // All env vars absent → must return an actionable error, no panic. + t.Setenv(auth.EnvServiceAccountFederatedToken, "") + t.Setenv(auth.EnvFederatedTokenFile, "") + t.Setenv(auth.EnvGitHubRequestURL, "") + t.Setenv(auth.EnvGitHubRequestToken, "") + t.Setenv(auth.EnvAzureOIDCRequestURI, "") + t.Setenv(auth.EnvAzureAccessToken, "") + + _, err := auth.OIDCTokenFunc() + if err == nil { + t.Fatal("OIDCTokenFunc() expected error when no OIDC source is available, got nil") + } +} + +func TestTokenFunc_GitHubURL_NoToken(t *testing.T) { + // URL present but token absent → should fall through to Azure / error. + t.Setenv(auth.EnvServiceAccountFederatedToken, "") + t.Setenv(auth.EnvFederatedTokenFile, "") + t.Setenv(auth.EnvGitHubRequestURL, "https://example.com") + t.Setenv(auth.EnvGitHubRequestToken, "") + t.Setenv(auth.EnvAzureOIDCRequestURI, "") + t.Setenv(auth.EnvAzureAccessToken, "") + + _, err := auth.OIDCTokenFunc() + if err == nil { + t.Fatal("OIDCTokenFunc() expected error when GitHub token is missing, got nil") + } +} diff --git a/internal/pkg/auth/service_account.go b/internal/pkg/auth/service_account.go index 1f1b01729..dd02dc81d 100644 --- a/internal/pkg/auth/service_account.go +++ b/internal/pkg/auth/service_account.go @@ -14,7 +14,6 @@ import ( type keyFlowInterface interface { GetAccessToken() (string, error) GetConfig() clients.KeyFlowConfig - GetToken() clients.TokenResponseBody RoundTrip(*http.Request) (*http.Response, error) } @@ -23,6 +22,10 @@ type tokenFlowInterface interface { RoundTrip(*http.Request) (*http.Response, error) } +type wifFlowInterface interface { + GetAccessToken() (string, error) +} + type keyFlowWithStorage struct { keyFlow *clients.KeyFlow } @@ -32,7 +35,7 @@ var _ http.RoundTripper = &keyFlowWithStorage{} // AuthenticateServiceAccount checks the type of the provided roundtripper, // authenticates the CLI accordingly and store the credentials. -// For the key flow, it fetches an access and refresh token from the Service Account API. +// For the key flow, it fetches an access token from the Service Account API. // For the token flow, it just stores the provided token and doesn't check if it is valid. // It returns the email associated with the service account // If disableWriting is set to true the credentials are not stored on disk (keyring, file). @@ -56,7 +59,6 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper, disableW } authFields[ACCESS_TOKEN] = accessToken - authFields[REFRESH_TOKEN] = flow.GetToken().RefreshToken authFields[SERVICE_ACCOUNT_KEY] = string(saKeyBytes) authFields[PRIVATE_KEY] = flow.GetConfig().PrivateKey case tokenFlowInterface: @@ -64,6 +66,17 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper, disableW authFlowType = AUTH_FLOW_SERVICE_ACCOUNT_TOKEN authFields[ACCESS_TOKEN] = flow.GetConfig().ServiceAccountToken + case wifFlowInterface: + p.Debug(print.DebugLevel, "authenticating using workload identity federation") + authFlowType = AUTH_FLOW_SERVICE_ACCOUNT_KEY + + accessToken, err := flow.GetAccessToken() + if err != nil { + p.Debug(print.ErrorLevel, "get workload identity access token: %v", err) + return "", "", &errors.ActivateServiceAccountError{} + } + authFields[ACCESS_TOKEN] = accessToken + disableWriting = true default: return "", "", fmt.Errorf("could not authenticate using any of the supported authentication flows (key and token): please report this issue") } @@ -82,6 +95,8 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper, disableW return "", "", fmt.Errorf("compute session expiration timestamp: %w", err) } authFields[SESSION_EXPIRES_AT_UNIX] = sessionExpiresAtUnix + // clear idp token endpoint as it is not set by default for service accounts + authFields[IDP_TOKEN_ENDPOINT] = "" if !disableWriting { err = SetAuthFlow(authFlowType) @@ -100,8 +115,6 @@ func AuthenticateServiceAccount(p *print.Printer, rt http.RoundTripper, disableW // initKeyFlowWithStorage initializes the keyFlow from the SDK and creates a keyFlowWithStorage struct that uses that keyFlow func initKeyFlowWithStorage() (*keyFlowWithStorage, error) { authFields := map[authFieldKey]string{ - ACCESS_TOKEN: "", - REFRESH_TOKEN: "", SERVICE_ACCOUNT_KEY: "", PRIVATE_KEY: "", TOKEN_CUSTOM_ENDPOINT: "", @@ -110,12 +123,6 @@ func initKeyFlowWithStorage() (*keyFlowWithStorage, error) { if err != nil { return nil, fmt.Errorf("get from auth storage: %w", err) } - if authFields[ACCESS_TOKEN] == "" { - return nil, fmt.Errorf("access token not set") - } - if authFields[REFRESH_TOKEN] == "" { - return nil, fmt.Errorf("refresh token not set") - } var serviceAccountKey = &clients.ServiceAccountKeyResponse{} err = json.Unmarshal([]byte(authFields[SERVICE_ACCOUNT_KEY]), serviceAccountKey) @@ -134,10 +141,6 @@ func initKeyFlowWithStorage() (*keyFlowWithStorage, error) { if err != nil { return nil, fmt.Errorf("initialize key flow: %w", err) } - err = keyFlow.SetToken(authFields[ACCESS_TOKEN], authFields[REFRESH_TOKEN]) - if err != nil { - return nil, fmt.Errorf("set access and refresh token: %w", err) - } // create keyFlowWithStorage roundtripper that stores the credentials after executing a request keyFlowWithStorage := &keyFlowWithStorage{ @@ -146,21 +149,26 @@ func initKeyFlowWithStorage() (*keyFlowWithStorage, error) { return keyFlowWithStorage, nil } -// The keyFlowWithStorage Roundtrip executes the keyFlow roundtrip and then stores the access and refresh tokens +// The keyFlowWithStorage Roundtrip executes the keyFlow roundtrip and then stores the access token func (kf *keyFlowWithStorage) RoundTrip(req *http.Request) (*http.Response, error) { resp, err := kf.keyFlow.RoundTrip(req) - token := kf.keyFlow.GetToken() - accessToken := token.AccessToken - refreshToken := token.RefreshToken + accessToken, getTokenErr := kf.keyFlow.GetAccessToken() + if getTokenErr != nil { + return nil, fmt.Errorf("get access token: %w", getTokenErr) + } + tokenValues := map[authFieldKey]string{ - ACCESS_TOKEN: accessToken, - REFRESH_TOKEN: refreshToken, + ACCESS_TOKEN: accessToken, } storageErr := SetAuthFieldMap(tokenValues) if storageErr != nil { - return nil, fmt.Errorf("set access and refresh token in the storage: %w", err) + // If the request was successful, but storing the token failed we still return the response and a nil error + if err == nil { + return resp, nil + } + return nil, fmt.Errorf("set access token in the storage: %w", err) } return resp, err diff --git a/internal/pkg/auth/service_account_test.go b/internal/pkg/auth/service_account_test.go index adc0f8bc5..250285f65 100644 --- a/internal/pkg/auth/service_account_test.go +++ b/internal/pkg/auth/service_account_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" "github.com/golang-jwt/jwt/v5" "github.com/stackitcloud/stackit-sdk-go/core/clients" @@ -66,6 +66,22 @@ func (f *tokenFlowMocked) RoundTrip(*http.Request) (*http.Response, error) { return nil, nil } +type wifFlowMocked struct { + accessToken string + getAccessTokenFail bool +} + +func (f *wifFlowMocked) GetAccessToken() (string, error) { + if f.getAccessTokenFail { + return "", fmt.Errorf("mock WIF error") + } + return f.accessToken, nil +} + +func (f *wifFlowMocked) RoundTrip(*http.Request) (*http.Response, error) { + return nil, nil +} + func TestAuthenticateServiceAccount(t *testing.T) { tests := []struct { description string @@ -152,8 +168,8 @@ func TestAuthenticateServiceAccount(t *testing.T) { flow = &http.Transport{} } - p := print.NewPrinter() - email, _, err := AuthenticateServiceAccount(p, flow, false) + params := testparams.NewTestParams() + email, _, err := AuthenticateServiceAccount(params.Printer, flow, false) if !tt.isValid { if err == nil { @@ -170,3 +186,76 @@ func TestAuthenticateServiceAccount(t *testing.T) { }) } } + +func TestAuthenticateServiceAccount_WIF(t *testing.T) { + // Build a signed test JWT that getEmailFromToken can parse. + testEmail := "ci@sa.stackit.cloud" + tok := jwt.NewWithClaims(jwt.SigningMethodHS256, &tokenClaims{ + Email: testEmail, + RegisteredClaims: jwt.RegisteredClaims{}, + }) + raw, err := tok.SignedString(accessTokenSigningKey) + if err != nil { + t.Fatalf("sign test token: %v", err) + } + + tests := []struct { + description string + accessToken string + getAccessTokenFail bool + disableWriting bool + isValid bool + }{ + { + description: "wif_success_no_credentials_written", + accessToken: raw, + disableWriting: false, // even when false, WIF forces no-write internally + isValid: true, + }, + { + description: "wif_get_access_token_fails", + getAccessTokenFail: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + keyring.MockInit() + config.InitConfig() + + flow := &wifFlowMocked{ + accessToken: tt.accessToken, + getAccessTokenFail: tt.getAccessTokenFail, + } + + params := testparams.NewTestParams() + email, _, err := AuthenticateServiceAccount(params.Printer, flow, tt.disableWriting) + + if !tt.isValid { + if err == nil { + t.Fatal("Expected error but no error was returned") + } + return + } + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if email != testEmail { + t.Fatalf("email = %q, want %q", email, testEmail) + } + + // Verify no credentials were written to the keyring / file. + // After a WIF authentication, the auth storage should not contain a + // service account key or private key. + storedKey, _ := GetAuthField(SERVICE_ACCOUNT_KEY) + if storedKey != "" { + t.Errorf("SERVICE_ACCOUNT_KEY was written to storage in WIF mode, want empty") + } + storedPrivKey, _ := GetAuthField(PRIVATE_KEY) + if storedPrivKey != "" { + t.Errorf("PRIVATE_KEY was written to storage in WIF mode, want empty") + } + }) + } +} diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index 5e857f6a7..686a0f677 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -27,16 +27,18 @@ const ( ) const ( - SESSION_EXPIRES_AT_UNIX authFieldKey = "session_expires_at_unix" - ACCESS_TOKEN authFieldKey = "access_token" - REFRESH_TOKEN authFieldKey = "refresh_token" - SERVICE_ACCOUNT_TOKEN authFieldKey = "service_account_token" - SERVICE_ACCOUNT_EMAIL authFieldKey = "service_account_email" - USER_EMAIL authFieldKey = "user_email" - SERVICE_ACCOUNT_KEY authFieldKey = "service_account_key" - PRIVATE_KEY authFieldKey = "private_key" - TOKEN_CUSTOM_ENDPOINT authFieldKey = "token_custom_endpoint" - IDP_TOKEN_ENDPOINT authFieldKey = "idp_token_endpoint" //nolint:gosec // linter false positive + SESSION_EXPIRES_AT_UNIX authFieldKey = "session_expires_at_unix" + ACCESS_TOKEN authFieldKey = "access_token" + REFRESH_TOKEN authFieldKey = "refresh_token" + SERVICE_ACCOUNT_TOKEN authFieldKey = "service_account_token" + SERVICE_ACCOUNT_EMAIL authFieldKey = "service_account_email" + USER_EMAIL authFieldKey = "user_email" + SERVICE_ACCOUNT_KEY authFieldKey = "service_account_key" + PRIVATE_KEY authFieldKey = "private_key" + TOKEN_CUSTOM_ENDPOINT authFieldKey = "token_custom_endpoint" + IDP_TOKEN_ENDPOINT authFieldKey = "idp_token_endpoint" //nolint:gosec // linter false positive + CACHE_ENCRYPTION_KEY authFieldKey = "cache_encryption_key" + CACHE_ENCRYPTION_KEY_AGE authFieldKey = "cache_encryption_key_age" ) const ( @@ -59,6 +61,8 @@ var authFieldKeys = []authFieldKey{ TOKEN_CUSTOM_ENDPOINT, IDP_TOKEN_ENDPOINT, authFlowType, + CACHE_ENCRYPTION_KEY, + CACHE_ENCRYPTION_KEY_AGE, } // All fields that are set when a user logs in diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index 37eeee33e..d7320917e 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -6,8 +6,9 @@ import ( "testing" "time" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/zalando/go-keyring" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) func TestSetGetAuthField(t *testing.T) { diff --git a/internal/pkg/auth/templates/login-successful.html b/internal/pkg/auth/templates/login-successful.html index d8519cad0..3e2d0a5ba 100644 --- a/internal/pkg/auth/templates/login-successful.html +++ b/internal/pkg/auth/templates/login-successful.html @@ -6,7 +6,7 @@ diff --git a/internal/pkg/auth/user_login.go b/internal/pkg/auth/user_login.go index 8ac94743e..5ad94d0a6 100644 --- a/internal/pkg/auth/user_login.go +++ b/internal/pkg/auth/user_login.go @@ -16,9 +16,10 @@ import ( "strings" "time" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "golang.org/x/oauth2" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) @@ -45,29 +46,22 @@ type InputValues struct { Logo string } +type UserAuthConfig struct { + // IsReauthentication defines if an expired user session should be renewed + IsReauthentication bool + // Port defines which port should be used for the UserAuthFlow callback + Port *int +} + type apiClient interface { Do(req *http.Request) (*http.Response, error) } // AuthorizeUser implements the PKCE OAuth2 flow. -func AuthorizeUser(p *print.Printer, isReauthentication bool) error { - idpWellKnownConfigURL, err := getIDPWellKnownConfigURL() - if err != nil { - return fmt.Errorf("get IDP well-known configuration: %w", err) - } - if idpWellKnownConfigURL != defaultWellKnownConfig { - p.Warn("You are using a custom identity provider well-known configuration (%s) for authentication.\n", idpWellKnownConfigURL) - err := p.PromptForEnter("Press Enter to proceed with the login...") - if err != nil { - return err - } - } - - p.Debug(print.DebugLevel, "get IDP well-known configuration from %s", idpWellKnownConfigURL) - httpClient := &http.Client{} - idpWellKnownConfig, err := parseWellKnownConfiguration(httpClient, idpWellKnownConfigURL) +func AuthorizeUser(p *print.Printer, authConfig UserAuthConfig) error { + idpWellKnownConfig, err := retrieveIDPWellKnownConfig(p) if err != nil { - return fmt.Errorf("parse IDP well-known configuration: %w", err) + return err } idpClientID, err := getIDPClientID() @@ -82,7 +76,7 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error { } } - if isReauthentication { + if authConfig.IsReauthentication { err := p.PromptForEnter("Your session has expired, press Enter to login again...") if err != nil { return err @@ -92,21 +86,38 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error { var redirectURL string var listener net.Listener var listenerErr error + var ipv6Listener net.Listener + var ipv6ListenerErr error var port int - for i := range configuredPortRange { - port = defaultPort + i - portString := fmt.Sprintf(":%s", strconv.Itoa(port)) + startingPort := defaultPort + portRange := configuredPortRange + if authConfig.Port != nil { + startingPort = *authConfig.Port + portRange = 1 + } + for i := range portRange { + port = startingPort + i + ipv4addr := fmt.Sprintf("127.0.0.1:%d", port) + ipv6addr := fmt.Sprintf("[::1]:%d", port) p.Debug(print.DebugLevel, "trying to bind port %d for login redirect", port) - listener, listenerErr = net.Listen("tcp", portString) + ipv6Listener, ipv6ListenerErr = net.Listen("tcp6", ipv6addr) + if ipv6ListenerErr != nil { + continue + } + listener, listenerErr = net.Listen("tcp4", ipv4addr) if listenerErr == nil { + _ = ipv6Listener.Close() redirectURL = fmt.Sprintf("http://localhost:%d", port) p.Debug(print.DebugLevel, "bound port %d for login redirect", port) break } p.Debug(print.DebugLevel, "unable to bind port %d for login redirect: %s", port, listenerErr) } + if ipv6ListenerErr != nil { + return fmt.Errorf("unable to bind port for login redirect, tried from port %d to %d: %w", startingPort, port, ipv6ListenerErr) + } if listenerErr != nil { - return fmt.Errorf("unable to bind port for login redirect, tried from port %d to %d: %w", defaultPort, port, err) + return fmt.Errorf("unable to bind port for login redirect, tried from port %d to %d: %w", startingPort, port, listenerErr) } conf := &oauth2.Config{ @@ -121,8 +132,13 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error { // Initialize the code verifier codeVerifier := oauth2.GenerateVerifier() + // Generate max age based on the session time limit + maxSessionDuration, err := getSessionExpiration() + if err != nil { + return err + } // Construct the authorization URL - authorizationURL := conf.AuthCodeURL("", oauth2.S256ChallengeOption(codeVerifier)) + authorizationURL := conf.AuthCodeURL("", oauth2.S256ChallengeOption(codeVerifier), oauth2.SetAuthURLParam("max_age", fmt.Sprintf("%d", int64(maxSessionDuration.Seconds())))) // Start a web server to listen on a callback URL mux := http.NewServeMux() @@ -244,6 +260,10 @@ func AuthorizeUser(p *print.Printer, isReauthentication bool) error { return fmt.Errorf("open browser to URL %s: %w", authorizationURL, err) } + // Print the link + p.Info("Your browser has been opened to visit:\n\n") + p.Info("%s\n\n", authorizationURL) + // Start the blocking web server loop // It will exit when the handlers get fired and call server.Close() p.Debug(print.DebugLevel, "listening for response from authentication server on %s", redirectURL) @@ -275,7 +295,7 @@ func getUserAccessAndRefreshTokens(idpWellKnownConfig *wellKnownConfig, clientID req, _ := http.NewRequest("POST", idpWellKnownConfig.TokenEndpoint, payload) req.Header.Add("content-type", "application/x-www-form-urlencoded") httpClient := &http.Client{} - res, err := httpClient.Do(req) + res, err := httpClient.Do(req) //nolint:gosec // URL from well known if err != nil { return "", "", fmt.Errorf("call access token endpoint: %w", err) } @@ -320,21 +340,25 @@ func cleanup(server *http.Server) { }() } -func openBrowser(pageUrl string) error { - var err error +func openBrowser(pageUrl string) (err error) { + err = utils.ValidateURLDomain(pageUrl) + if err != nil { + return err + } + switch runtime.GOOS { - case "linux": + case "freebsd", "linux": // We need to use the windows way on WSL, otherwise we do not pass query // parameters correctly. https://github.com/microsoft/WSL/issues/3832 if _, ok := os.LookupEnv("WSL_DISTRO_NAME"); !ok { - err = exec.Command("xdg-open", pageUrl).Start() + err = exec.Command("xdg-open", pageUrl).Start() //nolint:gosec // url is validated break } fallthrough case "windows": - err = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", pageUrl).Start() + err = exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", pageUrl).Start() //nolint:gosec // url is validated case "darwin": - err = exec.Command("open", pageUrl).Start() + err = exec.Command("open", pageUrl).Start() //nolint:gosec // url is validated default: err = fmt.Errorf("unsupported platform") } @@ -343,48 +367,3 @@ func openBrowser(pageUrl string) error { } return nil } - -// parseWellKnownConfiguration gets the well-known OpenID configuration from the provided URL and returns it as a JSON -// the method also stores the IDP token endpoint in the authentication storage -func parseWellKnownConfiguration(httpClient apiClient, wellKnownConfigURL string) (wellKnownConfig *wellKnownConfig, err error) { - req, _ := http.NewRequest("GET", wellKnownConfigURL, http.NoBody) - res, err := httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("make the request: %w", err) - } - - // Process the response - defer func() { - closeErr := res.Body.Close() - if closeErr != nil { - err = fmt.Errorf("close response body: %w", closeErr) - } - }() - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, fmt.Errorf("read response body: %w", err) - } - - err = json.Unmarshal(body, &wellKnownConfig) - if err != nil { - return nil, fmt.Errorf("unmarshal response: %w", err) - } - if wellKnownConfig == nil { - return nil, fmt.Errorf("nil well-known configuration response") - } - if wellKnownConfig.Issuer == "" { - return nil, fmt.Errorf("found no issuer") - } - if wellKnownConfig.AuthorizationEndpoint == "" { - return nil, fmt.Errorf("found no authorization endpoint") - } - if wellKnownConfig.TokenEndpoint == "" { - return nil, fmt.Errorf("found no token endpoint") - } - - err = SetAuthField(IDP_TOKEN_ENDPOINT, wellKnownConfig.TokenEndpoint) - if err != nil { - return nil, fmt.Errorf("set token endpoint in the authentication storage: %w", err) - } - return wellKnownConfig, err -} diff --git a/internal/pkg/auth/user_login_test.go b/internal/pkg/auth/user_login_test.go index 7b61a4af5..e6f4bf040 100644 --- a/internal/pkg/auth/user_login_test.go +++ b/internal/pkg/auth/user_login_test.go @@ -5,10 +5,6 @@ import ( "io" "net/http" "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/zalando/go-keyring" ) type apiClientMocked struct { @@ -28,83 +24,3 @@ func (a *apiClientMocked) Do(_ *http.Request) (*http.Response, error) { Body: io.NopCloser(strings.NewReader(a.getResponse)), }, nil } - -func TestParseWellKnownConfig(t *testing.T) { - tests := []struct { - name string - getFails bool - getResponse string - isValid bool - expected *wellKnownConfig - }{ - { - name: "success", - getFails: false, - getResponse: `{"issuer":"issuer","authorization_endpoint":"auth","token_endpoint":"token"}`, - isValid: true, - expected: &wellKnownConfig{ - Issuer: "issuer", - AuthorizationEndpoint: "auth", - TokenEndpoint: "token", - }, - }, - { - name: "get_fails", - getFails: true, - getResponse: "", - isValid: false, - expected: nil, - }, - { - name: "empty_response", - getFails: true, - getResponse: "", - isValid: false, - expected: nil, - }, - { - name: "missing_issuer", - getFails: true, - getResponse: `{"authorization_endpoint":"auth","token_endpoint":"token"}`, - isValid: false, - expected: nil, - }, - { - name: "missing_authorization", - getFails: true, - getResponse: `{"issuer":"issuer","token_endpoint":"token"}`, - isValid: false, - expected: nil, - }, - { - name: "missing_token", - getFails: true, - getResponse: `{"issuer":"issuer","authorization_endpoint":"auth"}`, - isValid: false, - expected: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - keyring.MockInit() - - testClient := apiClientMocked{ - tt.getFails, - tt.getResponse, - } - - got, err := parseWellKnownConfiguration(&testClient, "") - - if tt.isValid && err != nil { - t.Fatalf("expected no error, got %v", err) - } - if !tt.isValid && err == nil { - t.Fatalf("expected error, got none") - } - - if tt.isValid && !cmp.Equal(*got, *tt.expected) { - t.Fatalf("expected %v, got %v", tt.expected, got) - } - }) - } -} diff --git a/internal/pkg/auth/user_token_flow.go b/internal/pkg/auth/user_token_flow.go index 215db2fa3..5c775085a 100644 --- a/internal/pkg/auth/user_token_flow.go +++ b/internal/pkg/auth/user_token_flow.go @@ -6,15 +6,17 @@ import ( "io" "net/http" "net/url" + "strings" "time" "github.com/golang-jwt/jwt/v5" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) type userTokenFlow struct { printer *print.Printer - reauthorizeUserRoutine func(p *print.Printer, isReauthentication bool) error // Called if the user needs to login again + reauthorizeUserRoutine func(p *print.Printer, isReauthentication UserAuthConfig) error // Called if the user needs to login again client *http.Client authFlow AuthFlow accessToken string @@ -68,7 +70,7 @@ func (utf *userTokenFlow) RoundTrip(req *http.Request) (*http.Response, error) { } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", utf.accessToken)) - return utf.client.Do(req) + return utf.client.Do(req) //nolint:gosec // URL is request specific } func loadVarsFromStorage(utf *userTokenFlow) error { @@ -94,7 +96,12 @@ func loadVarsFromStorage(utf *userTokenFlow) error { } func reauthenticateUser(utf *userTokenFlow) error { - err := utf.reauthorizeUserRoutine(utf.printer, true) + err := utf.reauthorizeUserRoutine( + utf.printer, + UserAuthConfig{ + IsReauthentication: true, + }, + ) if err != nil { return fmt.Errorf("authenticate user: %w", err) } @@ -108,22 +115,28 @@ func reauthenticateUser(utf *userTokenFlow) error { return nil } -func TokenExpired(token string) (bool, error) { +func TokenExpirationTime(token string) (time.Time, error) { // We can safely use ParseUnverified because we are not authenticating the user at this point. // We're just checking the expiration time tokenParsed, _, err := jwt.NewParser().ParseUnverified(token, &jwt.RegisteredClaims{}) if err != nil { - return false, fmt.Errorf("parse access token: %w", err) + return time.Time{}, fmt.Errorf("parse access token: %w", err) } expirationTimestampNumeric, err := tokenParsed.Claims.GetExpirationTime() if err != nil { - return false, fmt.Errorf("get expiration timestamp from access token: %w", err) + return time.Time{}, fmt.Errorf("get expiration timestamp from access token: %w", err) } else if expirationTimestampNumeric == nil { - return false, nil + return time.Time{}, nil + } + return expirationTimestampNumeric.Time, nil +} + +func TokenExpired(token string) (bool, error) { + expirationTimestamp, err := TokenExpirationTime(token) + if err != nil || expirationTimestamp.Equal(time.Time{}) { + return false, err } - expirationTimestamp := expirationTimestampNumeric.Time - now := time.Now() - return now.After(expirationTimestamp), nil + return time.Now().After(expirationTimestamp), nil } // Refresh access and refresh tokens using a valid refresh token @@ -166,20 +179,20 @@ func buildRequestToRefreshTokens(utf *userTokenFlow) (*http.Request, error) { return nil, err } + form := url.Values{} + form.Set("grant_type", "refresh_token") + form.Set("client_id", idpClientID) + form.Set("refresh_token", utf.refreshToken) + req, err := http.NewRequest( http.MethodPost, utf.tokenEndpoint, - http.NoBody, + strings.NewReader(form.Encode()), ) if err != nil { return nil, err } - reqQuery := url.Values{} - reqQuery.Set("grant_type", "refresh_token") - reqQuery.Set("client_id", idpClientID) - reqQuery.Set("refresh_token", utf.refreshToken) - reqQuery.Set("token_format", "jwt") - req.URL.RawQuery = reqQuery.Encode() + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") return req, nil } diff --git a/internal/pkg/auth/user_token_flow_test.go b/internal/pkg/auth/user_token_flow_test.go index cd31350ad..f72fa5945 100644 --- a/internal/pkg/auth/user_token_flow_test.go +++ b/internal/pkg/auth/user_token_flow_test.go @@ -9,9 +9,11 @@ import ( "time" "github.com/golang-jwt/jwt/v5" - "github.com/spf13/cobra" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/zalando/go-keyring" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) const ( @@ -278,14 +280,12 @@ func TestRoundTrip(t *testing.T) { authorizeUserCalled: &authorizeUserCalled, tokensRefreshed: &tokensRefreshed, } - authorizeUserRoutine := func(_ *print.Printer, _ bool) error { + authorizeUserRoutine := func(_ *print.Printer, _ UserAuthConfig) error { return reauthorizeUser(authorizeUserContext) } - cmd := &cobra.Command{} - cmd.SetOut(io.Discard) // Suppresses console prints - - p := &print.Printer{Cmd: cmd} + params := testparams.NewTestParams() + p := params.Printer // Test RoundTripper rt := userTokenFlow{ @@ -388,17 +388,17 @@ func TestTokenExpired(t *testing.T) { token string expected bool }{ - { + { //nolint:gosec // not a real token, just a testcase desc: "token without exp", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c`, expected: false, }, - { + { //nolint:gosec // not a real token, just a testcase desc: "exp 0", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjB9.rIhVGrtR0B0gUYPZDnB6LZ_w7zckH_9qFZBWG4rCkRY`, expected: true, }, - { + { //nolint:gosec // not a real token, just a testcase desc: "exp 9007199254740991", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjIyNTc2MDkwNzExMTExMTExfQ.aStshPjoSKTIcBeESbLJWvbMVuw-XWInXcf1P7tiWaE`, expected: false, diff --git a/internal/pkg/auth/utils.go b/internal/pkg/auth/utils.go index 4fa431262..a1e7eaf77 100644 --- a/internal/pkg/auth/utils.go +++ b/internal/pkg/auth/utils.go @@ -1,10 +1,15 @@ package auth import ( + "encoding/json" "fmt" + "io" + "net/http" "github.com/spf13/viper" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) @@ -39,3 +44,79 @@ func getIDPClientID() (string, error) { return idpClientID, nil } + +func retrieveIDPWellKnownConfig(p *print.Printer) (*wellKnownConfig, error) { + idpWellKnownConfigURL, err := getIDPWellKnownConfigURL() + if err != nil { + return nil, fmt.Errorf("get IDP well-known configuration: %w", err) + } + if idpWellKnownConfigURL != defaultWellKnownConfig { + p.Warn("You are using a custom identity provider well-known configuration (%s) for authentication.\n", idpWellKnownConfigURL) + err := p.PromptForEnter("Press Enter to proceed with the login...") + if err != nil { + return nil, err + } + } + + p.Debug(print.DebugLevel, "get IDP well-known configuration from %s", idpWellKnownConfigURL) + httpClient := &http.Client{} + idpWellKnownConfig, err := parseWellKnownConfiguration(httpClient, idpWellKnownConfigURL) + if err != nil { + return nil, fmt.Errorf("parse IDP well-known configuration: %w", err) + } + return idpWellKnownConfig, nil +} + +// parseWellKnownConfiguration gets the well-known OpenID configuration from the provided URL and returns it as a JSON +// the method also stores the IDP token endpoint in the authentication storage +func parseWellKnownConfiguration(httpClient apiClient, wellKnownConfigURL string) (wellKnownConfig *wellKnownConfig, err error) { + req, _ := http.NewRequest("GET", wellKnownConfigURL, http.NoBody) + res, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("make the request: %w", err) + } + + // Process the response + defer func() { + closeErr := res.Body.Close() + if closeErr != nil { + err = fmt.Errorf("close response body: %w", closeErr) + } + }() + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("read response body: %w", err) + } + + err = json.Unmarshal(body, &wellKnownConfig) + if err != nil { + return nil, fmt.Errorf("unmarshal response: %w", err) + } + if wellKnownConfig == nil { + return nil, fmt.Errorf("nil well-known configuration response") + } + if wellKnownConfig.Issuer == "" { + return nil, fmt.Errorf("found no issuer") + } + if utils.ValidateURLDomain(wellKnownConfig.Issuer) != nil { + return nil, fmt.Errorf("issuer is invalid") + } + if wellKnownConfig.AuthorizationEndpoint == "" { + return nil, fmt.Errorf("found no authorization endpoint") + } + if utils.ValidateURLDomain(wellKnownConfig.AuthorizationEndpoint) != nil { + return nil, fmt.Errorf("authorization endpoint is invalid") + } + if wellKnownConfig.TokenEndpoint == "" { + return nil, fmt.Errorf("found no token endpoint") + } + if utils.ValidateURLDomain(wellKnownConfig.TokenEndpoint) != nil { + return nil, fmt.Errorf("token endpoint is invalid") + } + + err = SetAuthField(IDP_TOKEN_ENDPOINT, wellKnownConfig.TokenEndpoint) + if err != nil { + return nil, fmt.Errorf("set token endpoint in the authentication storage: %w", err) + } + return wellKnownConfig, err +} diff --git a/internal/pkg/auth/utils_test.go b/internal/pkg/auth/utils_test.go index 8112257d6..3b208dcd7 100644 --- a/internal/pkg/auth/utils_test.go +++ b/internal/pkg/auth/utils_test.go @@ -3,7 +3,10 @@ package auth import ( "testing" + "github.com/google/go-cmp/cmp" "github.com/spf13/viper" + "github.com/zalando/go-keyring" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) @@ -118,3 +121,83 @@ func TestGetIDPClientID(t *testing.T) { }) } } + +func TestParseWellKnownConfig(t *testing.T) { + tests := []struct { + name string + getFails bool + getResponse string + isValid bool + expected *wellKnownConfig + }{ + { + name: "success", + getFails: false, + getResponse: `{"issuer":"https://issuer.stackit.cloud/endpoint","authorization_endpoint":"https://auth.stackit.cloud/enpoint","token_endpoint":"https://token.stackit.cloud/endpoint"}`, + isValid: true, + expected: &wellKnownConfig{ //nolint:gosec // just a testcase; no credentials + Issuer: "https://issuer.stackit.cloud/endpoint", + AuthorizationEndpoint: "https://auth.stackit.cloud/enpoint", + TokenEndpoint: "https://token.stackit.cloud/endpoint", + }, + }, + { + name: "get_fails", + getFails: true, + getResponse: "", + isValid: false, + expected: nil, + }, + { + name: "empty_response", + getFails: true, + getResponse: "", + isValid: false, + expected: nil, + }, + { + name: "missing_issuer", + getFails: true, + getResponse: `{"authorization_endpoint":"https://auth.stackit.cloud/enpoint","token_endpoint":"https://token.stackit.cloud/endpoint"}`, + isValid: false, + expected: nil, + }, + { + name: "missing_authorization", + getFails: true, + getResponse: `{"issuer":"https://issuer.stackit.cloud/endpoint","token_endpoint":"https://token.stackit.cloud/endpoint"}`, + isValid: false, + expected: nil, + }, + { + name: "missing_token", + getFails: true, + getResponse: `{"issuer":"https://issuer.stackit.cloud/endpoint","authorization_endpoint":"https://auth.stackit.cloud/enpoint"}`, + isValid: false, + expected: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + keyring.MockInit() + + testClient := apiClientMocked{ + tt.getFails, + tt.getResponse, + } + + got, err := parseWellKnownConfiguration(&testClient, "") + + if tt.isValid && err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !tt.isValid && err == nil { + t.Fatalf("expected error, got none") + } + + if tt.isValid && !cmp.Equal(*got, *tt.expected) { + t.Fatalf("expected %v, got %v", tt.expected, got) + } + }) + } +} diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go index cf019ecb2..beaf87d12 100644 --- a/internal/pkg/cache/cache.go +++ b/internal/pkg/cache/cache.go @@ -1,26 +1,87 @@ package cache import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" "errors" "fmt" "os" "path/filepath" "regexp" + "strconv" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" ) var ( - cacheFolderPath string + cacheDirOverwrite string // for testing only + cacheFolderPath string + cacheEncryptionKey []byte identifierRegex = regexp.MustCompile("^[a-zA-Z0-9-]+$") ErrorInvalidCacheIdentifier = fmt.Errorf("invalid cache identifier") ) +const ( + cacheKeyMaxAge = 90 * 24 * time.Hour +) + func Init() error { - cacheDir, err := os.UserCacheDir() - if err != nil { - return fmt.Errorf("get user cache dir: %w", err) + var cacheDir string + if cacheDirOverwrite == "" { + var err error + cacheDir, err = os.UserCacheDir() + if err != nil { + return fmt.Errorf("get user cache dir: %w", err) + } + } else { + cacheDir = cacheDirOverwrite } + cacheFolderPath = filepath.Join(cacheDir, "stackit") + + // Encryption keys should only be used a limited number of times for aes-gcm. + // Thus, refresh the key periodically. This will invalidate all cached entries. + key, _ := auth.GetAuthField(auth.CACHE_ENCRYPTION_KEY) + age, _ := auth.GetAuthField(auth.CACHE_ENCRYPTION_KEY_AGE) + cacheEncryptionKey = nil + var keyAge time.Time + if age != "" { + ageSeconds, err := strconv.ParseInt(age, 10, 64) + if err == nil { + keyAge = time.Unix(ageSeconds, 0) + } + } + if key != "" && keyAge.Add(cacheKeyMaxAge).After(time.Now()) { + cacheEncryptionKey, _ = base64.StdEncoding.DecodeString(key) + // invalid key length + if len(cacheEncryptionKey) != 32 { + cacheEncryptionKey = nil + } + } + if len(cacheEncryptionKey) == 0 { + cacheEncryptionKey = make([]byte, 32) + _, err := rand.Read(cacheEncryptionKey) + if err != nil { + return fmt.Errorf("cache encryption key: %w", err) + } + key := base64.StdEncoding.EncodeToString(cacheEncryptionKey) + err = auth.SetAuthField(auth.CACHE_ENCRYPTION_KEY, key) + if err != nil { + return fmt.Errorf("save cache encryption key: %w", err) + } + err = auth.SetAuthField(auth.CACHE_ENCRYPTION_KEY_AGE, fmt.Sprint(time.Now().Unix())) + if err != nil { + return fmt.Errorf("save cache encryption key age: %w", err) + } + // cleanup old cache entries as they won't be readable anymore + if err := cleanupCache(); err != nil { + return err + } + } return nil } @@ -32,7 +93,21 @@ func GetObject(identifier string) ([]byte, error) { return nil, ErrorInvalidCacheIdentifier } - return os.ReadFile(filepath.Join(cacheFolderPath, identifier)) + data, err := os.ReadFile(filepath.Join(cacheFolderPath, identifier)) + if err != nil { + return nil, err + } + + block, err := aes.NewCipher(cacheEncryptionKey) + if err != nil { + return nil, err + } + aead, err := cipher.NewGCMWithRandomNonce(block) + if err != nil { + return nil, err + } + + return aead.Open(nil, nil, data, nil) } func PutObject(identifier string, data []byte) error { @@ -48,7 +123,17 @@ func PutObject(identifier string, data []byte) error { return err } - return os.WriteFile(filepath.Join(cacheFolderPath, identifier), data, 0o600) + block, err := aes.NewCipher(cacheEncryptionKey) + if err != nil { + return err + } + aead, err := cipher.NewGCMWithRandomNonce(block) + if err != nil { + return err + } + encrypted := aead.Seal(nil, nil, data, nil) + + return os.WriteFile(filepath.Join(cacheFolderPath, identifier), encrypted, 0o600) } func DeleteObject(identifier string) error { @@ -71,3 +156,26 @@ func validateCacheFolderPath() error { } return nil } + +func cleanupCache() error { + if err := validateCacheFolderPath(); err != nil { + return err + } + + entries, err := os.ReadDir(cacheFolderPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + return err + } + + for _, entry := range entries { + name := entry.Name() + err := DeleteObject(name) + if err != nil && !errors.Is(err, ErrorInvalidCacheIdentifier) { + return err + } + } + return nil +} diff --git a/internal/pkg/cache/cache_test.go b/internal/pkg/cache/cache_test.go index cc68c6590..fe0c143cc 100644 --- a/internal/pkg/cache/cache_test.go +++ b/internal/pkg/cache/cache_test.go @@ -6,10 +6,21 @@ import ( "path/filepath" "testing" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" + + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" ) -func TestGetObject(t *testing.T) { +func overwriteCacheDir(t *testing.T) func() { + cacheDirOverwrite = t.TempDir() + return func() { + cacheDirOverwrite = "" + } +} + +func TestGetObjectErrors(t *testing.T) { + defer overwriteCacheDir(t)() if err := Init(); err != nil { t.Fatalf("cache init failed: %s", err) } @@ -17,25 +28,16 @@ func TestGetObject(t *testing.T) { tests := []struct { description string identifier string - expectFile bool expectedErr error }{ - { - description: "identifier exists", - identifier: "test-cache-get-exists", - expectFile: true, - expectedErr: nil, - }, { description: "identifier does not exist", identifier: "test-cache-get-not-exists", - expectFile: false, expectedErr: os.ErrNotExist, }, { description: "identifier is invalid", identifier: "in../../valid", - expectFile: false, expectedErr: ErrorInvalidCacheIdentifier, }, } @@ -44,17 +46,6 @@ func TestGetObject(t *testing.T) { t.Run(tt.description, func(t *testing.T) { id := tt.identifier + "-" + uuid.NewString() - // setup - if tt.expectFile { - err := os.MkdirAll(cacheFolderPath, 0o750) - if err != nil { - t.Fatalf("create cache folder: %s", err.Error()) - } - path := filepath.Join(cacheFolderPath, id) - if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil { - t.Fatalf("setup: WriteFile (%s) failed", path) - } - } // test file, err := GetObject(id) @@ -62,19 +53,14 @@ func TestGetObject(t *testing.T) { t.Fatalf("returned error (%q) does not match %q", err.Error(), tt.expectedErr.Error()) } - if tt.expectFile { - if len(file) < 1 { - t.Fatalf("expected a file but byte array is empty (len %d)", len(file)) - } - } else { - if len(file) > 0 { - t.Fatalf("didn't expect a file, but byte array is not empty (len %d)", len(file)) - } + if len(file) > 0 { + t.Fatalf("didn't expect a file, but byte array is not empty (len %d)", len(file)) } }) } } func TestPutObject(t *testing.T) { + defer overwriteCacheDir(t)() if err := Init(); err != nil { t.Fatalf("cache init failed: %s", err) } @@ -128,6 +114,10 @@ func TestPutObject(t *testing.T) { // setup if tt.existingFile { + err := os.MkdirAll(cacheFolderPath, 0o750) + if err != nil { + t.Fatalf("create cache folder: %s", err.Error()) + } if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil { t.Fatalf("setup: WriteFile (%s) failed", path) } @@ -149,6 +139,7 @@ func TestPutObject(t *testing.T) { } func TestDeleteObject(t *testing.T) { + defer overwriteCacheDir(t)() if err := Init(); err != nil { t.Fatalf("cache init failed: %s", err) } @@ -186,8 +177,11 @@ func TestDeleteObject(t *testing.T) { // setup if tt.existingFile { + if err := os.MkdirAll(cacheFolderPath, 0o700); err != nil { + t.Fatalf("setup: MkdirAll (%s) failed: %v", path, err) + } if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil { - t.Fatalf("setup: WriteFile (%s) failed", path) + t.Fatalf("setup: WriteFile (%s) failed: %v", path, err) } } // test @@ -205,3 +199,90 @@ func TestDeleteObject(t *testing.T) { }) } } + +func clearKeys(t *testing.T) { + t.Helper() + err := auth.DeleteAuthField(auth.CACHE_ENCRYPTION_KEY) + if err != nil { + t.Fatalf("delete cache encryption key: %v", err) + } + err = auth.DeleteAuthField(auth.CACHE_ENCRYPTION_KEY_AGE) + if err != nil { + t.Fatalf("delete cache encryption key age: %v", err) + } +} + +func TestWriteAndRead(t *testing.T) { + for _, tt := range []struct { + name string + clearKeys bool + }{ + { + name: "normal", + }, + { + name: "fresh keys", + clearKeys: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + defer overwriteCacheDir(t)() + if tt.clearKeys { + clearKeys(t) + } + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } + + id := "test-cycle-" + uuid.NewString() + data := []byte("test-data") + err := PutObject(id, data) + if err != nil { + t.Fatalf("putobject failed: %v", err) + } + + readData, err := GetObject(id) + if err != nil { + t.Fatalf("getobject failed: %v", err) + } + + diff := cmp.Diff(data, readData) + if diff != "" { + t.Fatalf("unexpected data diff: %v", diff) + } + }) + } +} + +func TestCacheCleanup(t *testing.T) { + defer overwriteCacheDir(t)() + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } + + id := "test-cycle-" + uuid.NewString() + data := []byte("test-data") + err := PutObject(id, data) + if err != nil { + t.Fatalf("putobject failed: %v", err) + } + + clearKeys(t) + + // initialize again to trigger cache cleanup + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } + + _, err = GetObject(id) + if !errors.Is(err, os.ErrNotExist) { + t.Fatalf("getobject failed with unexpected error: %v", err) + } +} + +func TestInit(t *testing.T) { + // test that init without cache directory overwrite works + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 957d7c475..9e1084dac 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -17,13 +17,16 @@ const ( RegionKey = "region" SessionTimeLimitKey = "session_time_limit" VerbosityKey = "verbosity" + AssumeYesKey = "assume_yes" IdentityProviderCustomWellKnownConfigurationKey = "identity_provider_custom_well_known_configuration" IdentityProviderCustomClientIdKey = "identity_provider_custom_client_id" AllowedUrlDomainKey = "allowed_url_domain" AuthorizationCustomEndpointKey = "authorization_custom_endpoint" + AlbCustomEndpoint = "alb_custom _endpoint" DNSCustomEndpointKey = "dns_custom_endpoint" + EdgeCustomEndpointKey = "edge_custom_endpoint" LoadBalancerCustomEndpointKey = "load_balancer_custom_endpoint" LogMeCustomEndpointKey = "logme_custom_endpoint" MariaDBCustomEndpointKey = "mariadb_custom_endpoint" @@ -36,23 +39,30 @@ const ( RedisCustomEndpointKey = "redis_custom_endpoint" ResourceManagerEndpointKey = "resource_manager_custom_endpoint" SecretsManagerCustomEndpointKey = "secrets_manager_custom_endpoint" + KMSCustomEndpointKey = "kms_custom_endpoint" ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" ServiceEnablementCustomEndpointKey = "service_enablement_custom_endpoint" ServerBackupCustomEndpointKey = "serverbackup_custom_endpoint" ServerOsUpdateCustomEndpointKey = "serverosupdate_custom_endpoint" RunCommandCustomEndpointKey = "runcommand_custom_endpoint" + SfsCustomEndpointKey = "sfs_custom_endpoint" SKECustomEndpointKey = "ske_custom_endpoint" SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint" IaaSCustomEndpointKey = "iaas_custom_endpoint" TokenCustomEndpointKey = "token_custom_endpoint" GitCustomEndpointKey = "git_custom_endpoint" + CDNCustomEndpointKey = "cdn_custom_endpoint" + IntakeCustomEndpointKey = "intake_custom_endpoint" + LogsCustomEndpointKey = "logs_custom_endpoint" + VPNCustomEndpointKey = "vpn_custom_endpoint" ProjectNameKey = "project_name" DefaultProfileName = "default" AsyncDefault = false + AssumeYesDefault = false RegionDefault = "eu01" - SessionTimeLimitDefault = "2h" + SessionTimeLimitDefault = "12h" AllowedUrlDomainDefault = "stackit.cloud" ) @@ -75,36 +85,43 @@ var ConfigKeys = []string{ RegionKey, SessionTimeLimitKey, VerbosityKey, + AssumeYesKey, IdentityProviderCustomWellKnownConfigurationKey, IdentityProviderCustomClientIdKey, AllowedUrlDomainKey, + AlbCustomEndpoint, + AuthorizationCustomEndpointKey, + CDNCustomEndpointKey, DNSCustomEndpointKey, + EdgeCustomEndpointKey, + GitCustomEndpointKey, + IaaSCustomEndpointKey, + IntakeCustomEndpointKey, + KMSCustomEndpointKey, LoadBalancerCustomEndpointKey, LogMeCustomEndpointKey, + LogsCustomEndpointKey, MariaDBCustomEndpointKey, + MongoDBFlexCustomEndpointKey, ObjectStorageCustomEndpointKey, + ObservabilityCustomEndpointKey, OpenSearchCustomEndpointKey, PostgresFlexCustomEndpointKey, - ResourceManagerEndpointKey, - ObservabilityCustomEndpointKey, - AuthorizationCustomEndpointKey, - MongoDBFlexCustomEndpointKey, RabbitMQCustomEndpointKey, RedisCustomEndpointKey, ResourceManagerEndpointKey, - SecretsManagerCustomEndpointKey, - ServiceAccountCustomEndpointKey, - ServiceEnablementCustomEndpointKey, - ServerBackupCustomEndpointKey, - ServerOsUpdateCustomEndpointKey, RunCommandCustomEndpointKey, SKECustomEndpointKey, SQLServerFlexCustomEndpointKey, - IaaSCustomEndpointKey, + SecretsManagerCustomEndpointKey, + ServerBackupCustomEndpointKey, + ServerOsUpdateCustomEndpointKey, + ServiceAccountCustomEndpointKey, + ServiceEnablementCustomEndpointKey, + SfsCustomEndpointKey, TokenCustomEndpointKey, - GitCustomEndpointKey, } var defaultConfigFolderPath string @@ -172,6 +189,7 @@ func setConfigDefaults() { viper.SetDefault(IdentityProviderCustomClientIdKey, "") viper.SetDefault(AllowedUrlDomainKey, AllowedUrlDomainDefault) viper.SetDefault(DNSCustomEndpointKey, "") + viper.SetDefault(EdgeCustomEndpointKey, "") viper.SetDefault(ObservabilityCustomEndpointKey, "") viper.SetDefault(AuthorizationCustomEndpointKey, "") viper.SetDefault(MongoDBFlexCustomEndpointKey, "") @@ -180,6 +198,7 @@ func setConfigDefaults() { viper.SetDefault(PostgresFlexCustomEndpointKey, "") viper.SetDefault(ResourceManagerEndpointKey, "") viper.SetDefault(SecretsManagerCustomEndpointKey, "") + viper.SetDefault(KMSCustomEndpointKey, "") viper.SetDefault(ServiceAccountCustomEndpointKey, "") viper.SetDefault(ServiceEnablementCustomEndpointKey, "") viper.SetDefault(ServerBackupCustomEndpointKey, "") @@ -190,6 +209,10 @@ func setConfigDefaults() { viper.SetDefault(IaaSCustomEndpointKey, "") viper.SetDefault(TokenCustomEndpointKey, "") viper.SetDefault(GitCustomEndpointKey, "") + viper.SetDefault(IntakeCustomEndpointKey, "") + viper.SetDefault(AlbCustomEndpoint, "") + viper.SetDefault(LogsCustomEndpointKey, "") + viper.SetDefault(CDNCustomEndpointKey, "") } func getConfigFilePath(configFolder string) string { diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index db47ce5d3..83d48be2a 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -7,6 +7,8 @@ import ( "path/filepath" "regexp" + "github.com/spf13/viper" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/fileutils" "github.com/stackitcloud/stackit-cli/internal/pkg/print" @@ -79,8 +81,8 @@ func GetProfileFromEnv() (string, bool) { // CreateProfile creates a new profile. // If emptyProfile is true, it creates an empty profile. Otherwise, copies the config from the current profile to the new profile. // If setProfile is true, it sets the new profile as the active profile. -// If the profile already exists, it returns an error. -func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bool) error { +// If the profile already exists and ignoreExisting is false, it returns an error. +func CreateProfile(p *print.Printer, profile string, setProfile, ignoreExisting, emptyProfile bool) error { err := ValidateProfile(profile) if err != nil { return fmt.Errorf("validate profile: %w", err) @@ -98,6 +100,15 @@ func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bo // Error if the profile already exists _, err = os.Stat(configFolderPath) if err == nil { + if ignoreExisting { + if setProfile { + err = SetProfile(p, profile) + if err != nil { + return fmt.Errorf("set profile: %w", err) + } + } + return nil + } return fmt.Errorf("profile %q already exists", profile) } @@ -425,7 +436,13 @@ func ExportProfile(p *print.Printer, profile, exportPath string) error { return &errors.FileAlreadyExistsError{Filename: exportPath} } - err = fileutils.CopyFile(configFile, exportPath) + _, err = os.Stat(configFile) + if os.IsNotExist(err) { + // viper.SafeWriteConfigAs would not overwrite the target, so we use WriteConfigAs for the same behavior as CopyFile + err = viper.WriteConfigAs(exportPath) + } else { + err = fileutils.CopyFile(configFile, exportPath) + } if err != nil { return fmt.Errorf("export config file to %q: %w", exportPath, err) } diff --git a/internal/pkg/config/profiles_test.go b/internal/pkg/config/profiles_test.go index 327c9dcf8..296eb553a 100644 --- a/internal/pkg/config/profiles_test.go +++ b/internal/pkg/config/profiles_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" ) //go:embed template/test_profile.json @@ -157,8 +158,8 @@ func TestImportProfile(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - err := ImportProfile(p, tt.profile, tt.config, tt.setAsActive) + params := testparams.NewTestParams() + err := ImportProfile(params.Printer, tt.profile, tt.config, tt.setAsActive) if err != nil { if !tt.isValid { return @@ -172,8 +173,8 @@ func TestImportProfile(t *testing.T) { }) t.Cleanup(func() { - p := print.NewPrinter() - err := DeleteProfile(p, tt.profile) + params := testparams.NewTestParams() + err := DeleteProfile(params.Printer, tt.profile) if err != nil { if !tt.isValid { return @@ -208,9 +209,9 @@ func TestExportProfile(t *testing.T) { } // Create prerequisite profile - p := print.NewPrinter() + params := testparams.NewTestParams() profileName := "export-profile-test" - err = CreateProfile(p, profileName, true, false) + err = CreateProfile(params.Printer, profileName, true, false, false) if err != nil { t.Fatalf("could not create prerequisite profile, %v", err) } @@ -220,7 +221,7 @@ func TestExportProfile(t *testing.T) { if err != nil { fmt.Printf("could not clean up prerequisite profile %q, %v", profileName, err) } - }(p, profileName) + }(params.Printer, profileName) }) tests := []struct { @@ -256,8 +257,7 @@ func TestExportProfile(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - p := print.NewPrinter() - err := ExportProfile(p, tt.profile, tt.filePath) + err := ExportProfile(params.Printer, tt.profile, tt.filePath) if err != nil { if !tt.isValid { return diff --git a/internal/pkg/config/template/test_profile.json b/internal/pkg/config/template/test_profile.json index ab56ce66b..ed2702e7e 100644 --- a/internal/pkg/config/template/test_profile.json +++ b/internal/pkg/config/template/test_profile.json @@ -3,6 +3,7 @@ "async": false, "authorization_custom_endpoint": "", "dns_custom_endpoint": "", + "edge_custom_endpoint": "", "iaas_custom_endpoint": "", "identity_provider_custom_client_id": "", "identity_provider_custom_well_known_configuration": "", @@ -25,9 +26,10 @@ "serverbackup_custom_endpoint": "", "service_account_custom_endpoint": "", "service_enablement_custom_endpoint": "", - "session_time_limit": "2h", + "session_time_limit": "12h", + "sfs_custom_endpoint": "", "ske_custom_endpoint": "", "sqlserverflex_custom_endpoint": "", "token_custom_endpoint": "", "verbosity": "info" -} \ No newline at end of file +} diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 9c83fb4f1..d690259ea 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -1,10 +1,13 @@ package errors import ( + "encoding/json" + sysErrors "errors" "fmt" "strings" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" ) const ( @@ -18,6 +21,16 @@ You can configure it for all commands by running: or you can also set it through the environment variable [STACKIT_PROJECT_ID]` + MISSING_REGION = `the region is not currently set. + +It can be set on the command level by re-running your command with the --region flag. + +You can configure it for all commands by running: + + $ stackit config set --region xxx + +or you can also set it through the environment variable [STACKIT_REGION]` + EMPTY_UPDATE = `please specify at least one field to update. Get details on the available flags by re-running your command with the --help flag.` @@ -178,6 +191,12 @@ To list all profiles, run: $ stackit config profile list` FILE_ALREADY_EXISTS = `file %q already exists in the export path. Delete the existing file or define a different export path` + + FLAG_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET = `The flag %[1]q must be provided when %[2]q is set` + + MULTIPLE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET = `The flags %[1]v must be provided when one of the flags %[2]v is set` + + ONE_OF_THE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET = `One of the flags %[1]v must be provided when %[2]q is set` ) type ServerNicAttachMissingNicIdError struct { @@ -234,6 +253,12 @@ func (e *ProjectIdError) Error() string { return MISSING_PROJECT_ID } +type RegionError struct{} + +func (e *RegionError) Error() string { + return MISSING_REGION +} + type EmptyUpdateError struct{} func (e *EmptyUpdateError) Error() string { @@ -499,3 +524,163 @@ type FileAlreadyExistsError struct { } func (e *FileAlreadyExistsError) Error() string { return fmt.Sprintf(FILE_ALREADY_EXISTS, e.Filename) } + +type DependingFlagIsMissing struct { + MissingFlag string + SetFlag string +} + +func (e *DependingFlagIsMissing) Error() string { + return fmt.Sprintf(FLAG_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET, fmt.Sprintf("--%s", e.MissingFlag), fmt.Sprintf("--%s", e.SetFlag)) +} + +type MultipleFlagsAreMissing struct { + MissingFlags []string + SetFlags []string +} + +func (e *MultipleFlagsAreMissing) Error() string { + return fmt.Sprintf(MULTIPLE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET, e.MissingFlags, e.SetFlags) +} + +type OneOfFlagsIsMissing struct { + MissingFlags []string + SetFlag string +} + +func (e *OneOfFlagsIsMissing) Error() string { + return fmt.Sprintf(ONE_OF_THE_FLAGS_MUST_BE_PROVIDED_WHEN_ANOTHER_FLAG_IS_SET, e.MissingFlags, e.SetFlag) +} + +// ___FORMATTING_ERRORS_________________________________________________________ + +// InvalidFormatError indicates that an unsupported format was provided. +type InvalidFormatError struct { + Format string // The invalid format that was provided +} + +func (e *InvalidFormatError) Error() string { + if e.Format != "" { + return fmt.Sprintf("unsupported format provided: %s", e.Format) + } + return "unsupported format provided" +} + +// NewInvalidFormatError creates a new InvalidFormatError with the provided format. +func NewInvalidFormatError(format string) *InvalidFormatError { + return &InvalidFormatError{ + Format: format, + } +} + +// ___BUILD_REQUEST_ERRORS______________________________________________________ +// BuildRequestError indicates that a request could not be built. +type BuildRequestError struct { + Reason string // Optional: specific reason why the request failed to build + Err error // Optional: underlying error +} + +func (e *BuildRequestError) Error() string { + if e.Reason != "" && e.Err != nil { + return fmt.Sprintf("could not build request (%s): %v", e.Reason, e.Err) + } + if e.Reason != "" { + return fmt.Sprintf("could not build request: %s", e.Reason) + } + if e.Err != nil { + return fmt.Sprintf("could not build request: %v", e.Err) + } + return "could not build request" +} + +func (e *BuildRequestError) Unwrap() error { + return e.Err +} + +// NewBuildRequestError creates a new BuildRequestError with optional reason and underlying error. +func NewBuildRequestError(reason string, err error) *BuildRequestError { + return &BuildRequestError{ + Reason: reason, + Err: err, + } +} + +// ___REQUESTS_ERRORS___________________________________________________________ +// RequestFailedError indicates that an API request failed. +// If the provided error is an OpenAPI error, the status code and message from the error body will be included in the error message. +type RequestFailedError struct { + Err error // Optional: underlying error +} + +func (e *RequestFailedError) Error() string { + var msg = "request failed" + + if e.Err != nil { + var oApiErr *oapierror.GenericOpenAPIError + if sysErrors.As(e.Err, &oApiErr) { + // Extract status code from OpenAPI error header if it exists + if oApiErr.StatusCode > 0 { + msg += fmt.Sprintf(" (%d)", oApiErr.StatusCode) + } + + // Try to extract message from OpenAPI error body + if bodyMsg := extractOpenApiMessageFromBody(oApiErr.Body); bodyMsg != "" { + msg += fmt.Sprintf(": %s", bodyMsg) + } else if trimmedBody := strings.TrimSpace(string(oApiErr.Body)); trimmedBody != "" { + msg += fmt.Sprintf(": %s", trimmedBody) + } else { + // Otherwise use the Go error + msg += fmt.Sprintf(": %v", e.Err) + } + } else { + // If this can't be cased into a OpenApi error use the Go error + msg += fmt.Sprintf(": %v", e.Err) + } + } + + return msg +} + +func (e *RequestFailedError) Unwrap() error { + return e.Err +} + +// NewRequestFailedError creates a new RequestFailedError with optional details. +func NewRequestFailedError(err error) *RequestFailedError { + return &RequestFailedError{ + Err: err, + } +} + +// ___HELPERS___________________________________________________________________ +// extractOpenApiMessageFromBody attempts to parse a JSON body and extract the "message" +// field. It returns an empty string if parsing fails or if no message is found. +func extractOpenApiMessageFromBody(body []byte) string { + trimmedBody := strings.TrimSpace(string(body)) + // Return early if empty. + if trimmedBody == "" { + return "" + } + + // Try to unmarshal as a structured error first + var errorBody struct { + Message string `json:"message"` + } + if err := json.Unmarshal(body, &errorBody); err == nil && errorBody.Message != "" { + if msg := strings.TrimSpace(errorBody.Message); msg != "" { + return msg + } + } + + // If that fails, try to unmarshal as a plain string + var plainBody string + if err := json.Unmarshal(body, &plainBody); err == nil && plainBody != "" { + if msg := strings.TrimSpace(plainBody); msg != "" { + return msg + } + return "" + } + + // All parsing attempts failed or yielded no message + return "" +} diff --git a/internal/pkg/errors/errors_test.go b/internal/pkg/errors/errors_test.go index d2942e87f..8a1c3d117 100644 --- a/internal/pkg/errors/errors_test.go +++ b/internal/pkg/errors/errors_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" ) var cmd *cobra.Command @@ -13,6 +14,13 @@ var service *cobra.Command var resource *cobra.Command var operation *cobra.Command +var ( + testErrorMessage = "test error message" + errStringErrTest = errors.New(testErrorMessage) + errOpenApi404 = &oapierror.GenericOpenAPIError{StatusCode: 404, Body: []byte(`{"message":"not found"}`)} + errOpenApi500 = &oapierror.GenericOpenAPIError{StatusCode: 500, Body: []byte(`invalid-json`)} +) + func setupCmd() { cmd = &cobra.Command{ Use: "stackit", @@ -686,3 +694,238 @@ func TestAppendUsageTip(t *testing.T) { }) } } + +func TestInvalidFormatError(t *testing.T) { + type args struct { + format string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + format: "", + }, + want: "unsupported format provided", + }, + { + name: "with format", + args: args{ + format: "yaml", + }, + want: "unsupported format provided: yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&InvalidFormatError{Format: tt.args.format}).Error() + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestBuildRequestError(t *testing.T) { + type args struct { + reason string + err error + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + reason: "", + err: nil, + }, + want: "could not build request", + }, + { + name: "reason only", + args: args{ + reason: testErrorMessage, + err: nil, + }, + want: fmt.Sprintf("could not build request: %s", testErrorMessage), + }, + { + name: "error only", + args: args{ + reason: "", + err: errStringErrTest, + }, + want: fmt.Sprintf("could not build request: %s", testErrorMessage), + }, + { + name: "reason and error", + args: args{ + reason: testErrorMessage, + err: errStringErrTest, + }, + want: fmt.Sprintf("could not build request (%s): %s", testErrorMessage, testErrorMessage), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&BuildRequestError{Reason: tt.args.reason, Err: tt.args.err}).Error() + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestRequestFailedError(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args args + want string + }{ + { + name: "nil underlying", + args: args{ + err: nil, + }, + want: "request failed", + }, + { + name: "non-openapi error", + args: args{ + err: errStringErrTest, + }, + want: fmt.Sprintf("request failed: %s", testErrorMessage), + }, + { + name: "openapi error with message", + args: args{ + err: errOpenApi404, + }, + want: "request failed (404): not found", + }, + { + name: "openapi error without message", + args: args{ + err: errOpenApi500, + }, + want: "request failed (500): invalid-json", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&RequestFailedError{Err: tt.args.err}).Error() + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestExtractMessageFromBody(t *testing.T) { + type args struct { + body []byte + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty body", + args: args{ + body: []byte(""), + }, + want: "", + }, + { + name: "invalid json", + args: args{ + body: []byte("not-json"), + }, + want: "", + }, + { + name: "missing message field", + args: args{ + body: []byte(`{"error":"oops"}`), + }, + want: "", + }, + { + name: "with message field", + args: args{ + body: []byte(`{"message":"the reason"}`), + }, + want: "the reason", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractOpenApiMessageFromBody(tt.args.body) + if got != tt.want { + t.Errorf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestConstructorsReturnExpected(t *testing.T) { + buildRequestError := NewBuildRequestError(testErrorMessage, errStringErrTest) + + tests := []struct { + name string + got any + want any + }{ + { + name: "InvalidFormat format", + got: NewInvalidFormatError("fmt").Format, + want: "fmt", + }, + { + name: "BuildRequestError error", + got: buildRequestError.Err, + want: errStringErrTest, + }, + { + name: "BuildRequestError reason", + got: buildRequestError.Reason, + want: testErrorMessage, + }, + { + name: "RequestFailed error", + got: NewRequestFailedError(errStringErrTest).Err, + want: errStringErrTest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wantErr, wantIsErr := tt.want.(error) + gotErr, gotIsErr := tt.got.(error) + if wantIsErr { + if !gotIsErr { + t.Fatalf("expected error but got %T", tt.got) + } + if !errors.Is(gotErr, wantErr) { + t.Errorf("got error %v, want %v", gotErr, wantErr) + } + return + } + + if tt.got != tt.want { + t.Errorf("got %v, want %v", tt.got, tt.want) + } + }) + } +} diff --git a/internal/pkg/flags/email.go b/internal/pkg/flags/email.go new file mode 100644 index 000000000..203ea42a9 --- /dev/null +++ b/internal/pkg/flags/email.go @@ -0,0 +1,37 @@ +package flags + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" +) + +type emailFlag struct { + value string +} + +// Ensure the implementation satisfies the expected interface +var _ pflag.Value = &emailFlag{} + +// EmailFlag returns a flag which must be a valid Email. +func EmailFlag() *emailFlag { + return &emailFlag{} +} + +func (f *emailFlag) String() string { + return f.value +} + +func (f *emailFlag) Set(value string) error { + isEmail := value != "" && strings.Contains(value, "@") + if !isEmail { + return fmt.Errorf("invalid email address: %s", value) + } + f.value = value + return nil +} + +func (f *emailFlag) Type() string { + return "string" +} diff --git a/internal/pkg/flags/email_test.go b/internal/pkg/flags/email_test.go new file mode 100644 index 000000000..24af1f7c5 --- /dev/null +++ b/internal/pkg/flags/email_test.go @@ -0,0 +1,61 @@ +package flags + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestEmailFlag(t *testing.T) { + tests := []struct { + description string + value string + isValid bool + }{ + { + description: "valid", + value: "test@test", + isValid: true, + }, + { + description: "empty", + value: "", + isValid: false, + }, + { + description: "invalid", + value: "invalid-email", + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + flag := EmailFlag() + cmd := &cobra.Command{ + Use: "test", + RunE: func(_ *cobra.Command, _ []string) error { + return nil + }, + } + cmd.Flags().Var(flag, "test-flag", "test") + + err := cmd.Flags().Set("test-flag", tt.value) + + if !tt.isValid && err == nil { + t.Fatalf("did not fail on invalid input") + } + if !tt.isValid { + return + } + + if err != nil { + t.Fatalf("failed on valid input: %v", err) + } + value := FlagToStringValue(nil, cmd, "test-flag") + if value != tt.value { + t.Fatalf("flag did not return set value") + } + }) + } +} diff --git a/internal/pkg/flags/enum.go b/internal/pkg/flags/enum.go deleted file mode 100644 index 7ac3a531e..000000000 --- a/internal/pkg/flags/enum.go +++ /dev/null @@ -1,65 +0,0 @@ -package flags - -import ( - "fmt" - "strings" - - "github.com/spf13/pflag" -) - -type enumFlag struct { - ignoreCase bool - options []string - value string -} - -// Ensure the implementation satisfies the expected interface -var _ pflag.Value = &enumFlag{} - -// EnumFlag returns a flag which must be one of the given values. -// If ignoreCase is true, flag value is returned in lower case. -func EnumFlag(ignoreCase bool, defaultValue string, options ...string) *enumFlag { - if defaultValue == "" { - return &enumFlag{ignoreCase: ignoreCase, options: options} - } - - validDefault := false - for _, o := range options { - if !ignoreCase && defaultValue == o { - validDefault = true - break - } - if ignoreCase && strings.EqualFold(defaultValue, o) { - validDefault = true - break - } - } - if !validDefault { - panic(fmt.Sprintf("default value %q is not one of %q", defaultValue, options)) - } - - return &enumFlag{ignoreCase: ignoreCase, options: options, value: defaultValue} -} - -func (f *enumFlag) String() string { - return f.value -} - -func (f *enumFlag) Set(value string) error { - for _, o := range f.options { - if !f.ignoreCase && value == o { - f.value = value - return nil - } - if f.ignoreCase && strings.EqualFold(value, o) { - f.value = strings.ToLower(value) - return nil - } - } - - return fmt.Errorf("expected one of %q", f.options) -} - -func (f *enumFlag) Type() string { - return "string" -} diff --git a/internal/pkg/flags/enumslice.go b/internal/pkg/flags/enumslice.go deleted file mode 100644 index 4b68c9025..000000000 --- a/internal/pkg/flags/enumslice.go +++ /dev/null @@ -1,76 +0,0 @@ -package flags - -import ( - "fmt" - "strings" - - "github.com/spf13/pflag" -) - -type enumSliceFlag struct { - ignoreCase bool - options []string - value []string - valueSet bool -} - -// Ensure the implementation satisfies the expected interface -var _ pflag.Value = &enumFlag{} - -// EnumSliceFlag returns a flag which is a slice which values must be one of the given values. -// If ignoreCase is true, values are returned in lower case. -func EnumSliceFlag(ignoreCase bool, defaultValues []string, options ...string) *enumSliceFlag { - f := &enumSliceFlag{ignoreCase: ignoreCase, options: options} - err := f.appendToValue(defaultValues) - if err != nil { - panic(err) - } - return f -} - -func (f *enumSliceFlag) appendToValue(values []string) error { - for _, v := range values { - v = strings.TrimSpace(v) - - foundValid := false - for _, o := range f.options { - if !f.ignoreCase && v == o { - f.value = append(f.value, v) - foundValid = true - break - } else if f.ignoreCase && strings.EqualFold(v, o) { - f.value = append(f.value, strings.ToLower(v)) - foundValid = true - break - } - } - - if !foundValid { - return fmt.Errorf("found value %q, expected one of %q", v, f.options) - } - } - return nil -} - -func (f *enumSliceFlag) String() string { - return "[" + strings.Join(f.value, ",") + "]" -} - -func (f *enumSliceFlag) Set(value string) error { - // If the default value is still set, remove it - // (Since we're going to append the incoming values to f.value) - if !f.valueSet { - f.value = []string{} - f.valueSet = true - } - - if value == "" { - return fmt.Errorf("value cannot be empty") - } - values := strings.Split(value, ",") - return f.appendToValue(values) -} - -func (f *enumSliceFlag) Type() string { - return "stringSlice" -} diff --git a/internal/pkg/flags/flag_to_value.go b/internal/pkg/flags/flag_to_value.go index 6385ba65a..c53aa751e 100644 --- a/internal/pkg/flags/flag_to_value.go +++ b/internal/pkg/flags/flag_to_value.go @@ -5,6 +5,7 @@ import ( "time" "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) @@ -47,6 +48,20 @@ func FlagToStringSliceValue(p *print.Printer, cmd *cobra.Command, flag string) [ return nil } +// Returns the flag's value as a []string. +// Returns nil if flag is not set, if its value can not be converted to []string, or if the flag does not exist. +func FlagToStringArrayValue(p *print.Printer, cmd *cobra.Command, flag string) []string { + value, err := cmd.Flags().GetStringArray(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag to string array value: %v", err) + return nil + } + if !cmd.Flag(flag).Changed { + return nil + } + return value +} + // Returns a pointer to the flag's value. // Returns nil if the flag is not set, if its value can not be converted to map[string]string, or if the flag does not exist. func FlagToStringToStringPointer(p *print.Printer, cmd *cobra.Command, flag string) *map[string]string { //nolint:gocritic //convenient for setting the SDK payload @@ -61,6 +76,36 @@ func FlagToStringToStringPointer(p *print.Printer, cmd *cobra.Command, flag stri return nil } +func FlagToStringToAny(p *print.Printer, cmd *cobra.Command, flag string) map[string]any { + value, err := cmd.Flags().GetStringToString(flag) + r := make(map[string]any, len(value)) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag to string to any value: %v", err) + return r + } + if !cmd.Flag(flag).Changed { + return nil + } + for k, v := range value { + r[k] = v + } + return r +} + +// Returns a pointer to the flag's value. +// Returns nil if the flag is not set, if its value can not be converted to int, or if the flag does not exist. +func FlagToIntPointer(p *print.Printer, cmd *cobra.Command, flag string) *int { + value, err := cmd.Flags().GetInt(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag to Uint64 pointer: %v", err) + return nil + } + if cmd.Flag(flag).Changed { + return &value + } + return nil +} + // Returns a pointer to the flag's value. // Returns nil if the flag is not set, if its value can not be converted to int64, or if the flag does not exist. func FlagToInt64Pointer(p *print.Printer, cmd *cobra.Command, flag string) *int64 { @@ -75,6 +120,20 @@ func FlagToInt64Pointer(p *print.Printer, cmd *cobra.Command, flag string) *int6 return nil } +// Returns a pointer to the flag's value. +// Returns nil if the flag is not set, if its value can not be converted to int64, or if the flag does not exist. +func FlagToInt32Pointer(p *print.Printer, cmd *cobra.Command, flag string) *int32 { + value, err := cmd.Flags().GetInt32(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag to Int pointer: %v", err) + return nil + } + if cmd.Flag(flag).Changed { + return &value + } + return nil +} + // Returns a pointer to the flag's value. // Returns nil if the flag is not set, if its value can not be converted to string, or if the flag does not exist. func FlagToStringPointer(p *print.Printer, cmd *cobra.Command, flag string) *string { @@ -148,6 +207,17 @@ func FlagWithDefaultToInt64Value(p *print.Printer, cmd *cobra.Command, flag stri return value } +// Returns the int32 value set on the flag. If no value is set, returns the flag's default value. +// Returns 0 if the flag value can not be converted to int32 or if the flag does not exist. +func FlagWithDefaultToInt32Value(p *print.Printer, cmd *cobra.Command, flag string) int32 { + value, err := cmd.Flags().GetInt32(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert flag with default to Int32 value: %v", err) + return 0 + } + return value +} + // Returns the string value set on the flag. If no value is set, returns the flag's default value. // Returns nil if the flag value can not be converted to string or if the flag does not exist. func FlagWithDefaultToStringValue(p *print.Printer, cmd *cobra.Command, flag string) string { diff --git a/internal/pkg/flags/flag_to_value_test.go b/internal/pkg/flags/flag_to_value_test.go new file mode 100644 index 000000000..cc9605796 --- /dev/null +++ b/internal/pkg/flags/flag_to_value_test.go @@ -0,0 +1,187 @@ +package flags + +import ( + "fmt" + "reflect" + "testing" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func TestFlagToStringToStringPointer(t *testing.T) { + const flagName = "labels" + + tests := []struct { + name string + flagValue *string + want *map[string]string + }{ + { + name: "flag unset", + flagValue: nil, + want: nil, + }, + { + name: "flag set with single value", + flagValue: utils.Ptr("foo=bar"), + want: &map[string]string{ + "foo": "bar", + }, + }, + { + name: "flag set with multiple values", + flagValue: utils.Ptr("foo=bar,label1=value1,label2=value2"), + want: &map[string]string{ + "foo": "bar", + "label1": "value1", + "label2": "value2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + // create a new, simple test command with a string-to-string flag + cmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "greet", + Short: "A simple greeting command", + Long: "A simple greeting command", + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("Hello world") + }, + } + cmd.Flags().StringToString(flagName, nil, "Labels are key-value string pairs.") + return cmd + }() + + // set the flag value if a value use given, else consider the flag unset + if tt.flagValue != nil { + err := cmd.Flags().Set(flagName, *tt.flagValue) + if err != nil { + t.Error(err) + } + } + + if got := FlagToStringToStringPointer(params.Printer, cmd, flagName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FlagToStringToStringPointer() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFlagToStringArrayValue(t *testing.T) { + const flagName = "geofencing" + tests := []struct { + name string + flagValues []string + want []string + }{ + { + name: "flag unset", + flagValues: nil, + want: nil, + }, + { + name: "single flag value", + flagValues: []string{ + "https://foo.example.com DE,CH", + }, + want: []string{ + "https://foo.example.com DE,CH", + }, + }, + { + name: "multiple flag value", + flagValues: []string{ + "https://foo.example.com DE,CH", + "https://bar.example.com AT", + }, + want: []string{ + "https://foo.example.com DE,CH", + "https://bar.example.com AT", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "greet", + Short: "A simple greeting command", + Long: "A simple greeting command", + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("Hello world") + }, + } + cmd.Flags().StringArray(flagName, []string{}, "url to multiple region codes, repeatable") + return cmd + }() + // set the flag value if a value use given, else consider the flag unset + if tt.flagValues != nil { + for _, val := range tt.flagValues { + err := cmd.Flags().Set(flagName, val) + if err != nil { + t.Error(err) + } + } + } + + if got := FlagToStringArrayValue(params.Printer, cmd, flagName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FlagToStringArrayValue() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFlagToInt32Pointer(t *testing.T) { + const flagName = "limit" + tests := []struct { + name string + flagValue *string + want *int32 + }{ + { + name: "flag unset", + flagValue: nil, + want: nil, + }, + { + name: "flag value", + flagValue: utils.Ptr("42"), + want: utils.Ptr(int32(42)), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := testparams.NewTestParams() + cmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "greet", + Short: "A simple greeting command", + Long: "A simple greeting command", + Run: func(_ *cobra.Command, _ []string) { + fmt.Println("Hello world") + }, + } + cmd.Flags().Int32(flagName, 0, "limit") + return cmd + }() + // set the flag value if a value use given, else consider the flag unset + if tt.flagValue != nil { + err := cmd.Flags().Set(flagName, *tt.flagValue) + if err != nil { + t.Error(err) + } + } + + if got := FlagToInt32Pointer(params.Printer, cmd, flagName); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FlagToInt32Pointer() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/flags/flags_test.go b/internal/pkg/flags/flags_test.go index 1021941de..ddc97fdd7 100644 --- a/internal/pkg/flags/flags_test.go +++ b/internal/pkg/flags/flags_test.go @@ -3,7 +3,6 @@ package flags import ( "fmt" "reflect" - "strings" "testing" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -12,268 +11,6 @@ import ( "github.com/spf13/cobra" ) -func TestEnumFlag(t *testing.T) { - options := []string{"foo", "BaR"} - - tests := []struct { - description string - ignoreCase bool - value string - isValid bool - }{ - { - description: "valid", - value: "foo", - isValid: true, - }, - { - description: "empty", - value: "", - isValid: false, - }, - { - description: "invalid 1", - value: "ba", - isValid: false, - }, - { - description: "invalid 2", - value: "foo ", - isValid: false, - }, - { - description: "invalid 3", - value: "bar", - isValid: false, - }, - { - description: "ignore case - valid 1", - ignoreCase: true, - value: "foo", - isValid: true, - }, - { - description: "ignore case - valid 2", - ignoreCase: true, - value: "fOO", - isValid: true, - }, - { - description: "ignore case - valid 3", - ignoreCase: true, - value: "bar", - isValid: true, - }, - { - description: "ignore case - invalid 1", - ignoreCase: true, - value: "ba", - isValid: false, - }, - { - description: "ignore case - invalid 2", - ignoreCase: true, - value: "foo ", - isValid: false, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - flag := EnumFlag(tt.ignoreCase, "", options...) - cmd := &cobra.Command{ - Use: "test", - RunE: func(_ *cobra.Command, _ []string) error { - return nil - }, - } - cmd.Flags().Var(flag, "test-flag", "test") - - err := cmd.Flags().Set("test-flag", tt.value) - - if !tt.isValid && err == nil { - t.Fatalf("did not fail on invalid input") - } - if !tt.isValid { - return - } - - if err != nil { - t.Fatalf("failed on valid input: %v", err) - } - value := FlagToStringValue(nil, cmd, "test-flag") - if !tt.ignoreCase && value != tt.value { - t.Fatalf("flag did not return set value") - } - if tt.ignoreCase && !strings.EqualFold(value, tt.value) { - t.Fatalf("flag did not return set value") - } - }) - } -} - -func TestEnumSliceFlag(t *testing.T) { - validOption1 := "foo" - validOption2 := "BaR" - validOption3 := "baz" - - validOption2Lower := strings.ToLower(validOption2) - - options := []string{validOption1, validOption2, validOption3} - - tests := []struct { - description string - ignoreCase bool - defaultValue []string - value1 *string - value2 *string - expectedValue []string - isValid bool - }{ - { - description: "valid two single values", - value1: utils.Ptr(validOption1), - value2: utils.Ptr(validOption2), - expectedValue: []string{validOption1, validOption2}, - isValid: true, - }, - { - description: "valid list value", - value1: utils.Ptr(fmt.Sprintf("%s,%s", validOption1, validOption2)), - expectedValue: []string{validOption1, validOption2}, - isValid: true, - }, - { - description: "valid list value and single value", - value1: utils.Ptr(fmt.Sprintf("%s,%s", validOption1, validOption2)), - value2: utils.Ptr(validOption3), - expectedValue: []string{validOption1, validOption2, validOption3}, - isValid: true, - }, - { - description: "valid two list values", - value1: utils.Ptr(fmt.Sprintf("%s,%s", validOption1, validOption2)), - value2: utils.Ptr(fmt.Sprintf("%s,%s", validOption2, validOption3)), - expectedValue: []string{validOption1, validOption2, validOption2, validOption3}, - isValid: true, - }, - { - description: "invalid value", - value1: utils.Ptr("invalid-value"), - value2: utils.Ptr(validOption1), - isValid: false, - }, - { - description: "invalid value in list", - value1: utils.Ptr(fmt.Sprintf("invalid-value,%s", validOption1)), - isValid: false, - }, - { - description: "invalid empty value", - value1: utils.Ptr(""), - isValid: false, - }, - { - description: "invalid empty value in list", - value1: utils.Ptr(fmt.Sprintf(",%s", validOption1)), - isValid: false, - }, - { - description: "no values", - expectedValue: []string{}, - isValid: true, - }, - { - description: "ignore case - valid single value", - value1: utils.Ptr(validOption2Lower), - ignoreCase: true, - expectedValue: []string{validOption2Lower}, - isValid: true, - }, - { - description: "ignore case - valid in list", - value1: utils.Ptr(fmt.Sprintf("%s,%s", validOption1, validOption2Lower)), - ignoreCase: true, - expectedValue: []string{validOption1, validOption2Lower}, - isValid: true, - }, - { - description: "ignore case - invalid single value", - value1: utils.Ptr("ba"), - ignoreCase: true, - isValid: false, - }, - { - description: "ignore case - invalid in list", - value1: utils.Ptr(fmt.Sprintf("%s,%s", validOption1, "ba")), - ignoreCase: true, - isValid: false, - }, - { - description: "default value", - defaultValue: []string{validOption1, validOption2}, - expectedValue: []string{validOption1, validOption2}, - isValid: true, - }, - { - description: "default value - set value", - defaultValue: []string{validOption1, validOption2}, - value1: utils.Ptr(validOption1), - expectedValue: []string{validOption1}, - isValid: true, - }, - { - description: "ignore case - default value", - defaultValue: []string{validOption2}, - ignoreCase: true, - expectedValue: []string{validOption2Lower}, - isValid: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - flag := EnumSliceFlag(tt.ignoreCase, tt.defaultValue, options...) - cmd := &cobra.Command{ - Use: "test", - RunE: func(_ *cobra.Command, _ []string) error { - return nil - }, - } - cmd.Flags().Var(flag, "test-flag", "test") - - var err1, err2 error - if tt.value1 != nil { - err1 = cmd.Flags().Set("test-flag", *tt.value1) - } - if tt.value2 != nil { - err2 = cmd.Flags().Set("test-flag", *tt.value2) - } - - if !tt.isValid && err1 == nil && err2 == nil { - t.Fatalf("did not fail on invalid input") - } - if !tt.isValid { - return - } - - if err1 != nil { - t.Fatalf("failed on valid input: %v", err1) - } - if err2 != nil { - t.Fatalf("failed on valid input: %v", err2) - } - value, err := cmd.Flags().GetStringSlice("test-flag") - if err != nil { - t.Fatalf("failed to get value: %v", err) - } - if !reflect.DeepEqual(tt.expectedValue, value) { - t.Fatalf("flag did not return set value (expected %s, got %s)", tt.expectedValue, value) - } - }) - } -} - func TestUUIDFlag(t *testing.T) { tests := []struct { description string diff --git a/internal/pkg/flags/secret.go b/internal/pkg/flags/secret.go new file mode 100644 index 000000000..9dc0ba1b5 --- /dev/null +++ b/internal/pkg/flags/secret.go @@ -0,0 +1,89 @@ +package flags + +import ( + "fmt" + "io/fs" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type secretFlag struct { + printer *print.Printer + fs fs.FS + value string + name string +} + +func SecretFlag(name string, params *types.CmdParams) *secretFlag { + f := &secretFlag{ + printer: params.Printer, + fs: params.Fs, + name: name, + } + return f +} + +var _ pflag.Value = &secretFlag{} + +func (f *secretFlag) String() string { + return f.value +} + +func (f *secretFlag) Set(value string) error { + if strings.HasPrefix(value, "@") { + path := strings.Trim(value[1:], `"'`) + bytes, err := fs.ReadFile(f.fs, path) + if err != nil { + return fmt.Errorf("reading secret %s: %w", f.name, err) + } + f.value = string(bytes) + return nil + } + f.printer.Warn("Passing a secret value on the command line is insecure and deprecated. This usage will stop working October 2026.\n") + f.value = value + return nil +} + +func (f *secretFlag) Type() string { + return "string" +} + +func (f *secretFlag) Usage() string { + name := cases.Title(language.AmericanEnglish).String(f.name) + return fmt.Sprintf("%s. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.", name) +} + +func SecretFlagToStringPointer(p *print.Printer, cmd *cobra.Command, flag string) *string { + value, err := cmd.Flags().GetString(flag) + if err != nil { + p.Debug(print.ErrorLevel, "convert secret flag to string pointer: %v", err) + return nil + } + if value == "" { + input, err := p.PromptForPassword(fmt.Sprintf("enter %s: ", flag)) + if err != nil { + p.Debug(print.ErrorLevel, "convert secret flag %q to string pointer: %v", flag, err) + return nil + } + return &input + } + if cmd.Flag(flag).Changed { + return &value + } + return nil +} + +func SecretFlagToString(p *print.Printer, cmd *cobra.Command, flag string) string { + pointer := SecretFlagToStringPointer(p, cmd, flag) + if pointer == nil { + return "" + } + return *pointer +} diff --git a/internal/pkg/flags/secret_test.go b/internal/pkg/flags/secret_test.go new file mode 100644 index 000000000..53b8b417f --- /dev/null +++ b/internal/pkg/flags/secret_test.go @@ -0,0 +1,162 @@ +package flags + +import ( + "fmt" + "io" + "strings" + "testing" + "testing/fstest" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type testFile struct { + path, content string +} + +func TestSecretFlag(t *testing.T) { + t.Parallel() + tests := []struct { + name string + value string + want *string + file *testFile + stdin string + wantErr bool + wantStdErr string + }{ + { + name: "no value: prompts", + value: "", + want: utils.Ptr("from stdin"), + stdin: "from stdin", + }, + { + name: "a value: prints deprecation", + value: "a value", + want: utils.Ptr("a value"), + wantStdErr: "Warning: Passing a secret value on the command line is insecure and deprecated. This usage will stop working October 2026.\n", + }, + { + name: "from an existing file", + value: "@some-file.txt", + want: utils.Ptr("from file"), + file: &testFile{ + path: "some-file.txt", + content: "from file", + }, + }, + { + name: "from a non-existing file", + value: "@some-file-with-typo.txt", + wantErr: true, + file: &testFile{ + path: "some-file.txt", + content: "from file", + }, + }, + { + name: "from an existing double-quoted file", + value: `@"some-file.txt"`, + want: utils.Ptr("from file"), + file: &testFile{ + path: "some-file.txt", + content: "from file", + }, + }, + { + name: "from an existing single-quoted file", + value: "@'some-file.txt'", + want: utils.Ptr("from file"), + file: &testFile{ + path: "some-file.txt", + content: "from file", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + params := testparams.NewTestParams() + if tt.file != nil { + params.Fs = fstest.MapFS{ + tt.file.path: &fstest.MapFile{ + Data: []byte(tt.file.content), + }, + } + } + flag := SecretFlag("test", params.CmdParams) + cmd := cobra.Command{} + cmd.Flags().Var(flag, "test", flag.Usage()) + if tt.stdin != "" { + params.In.WriteString(tt.stdin) + params.In.WriteString("\n") + } + + if tt.value != "" { // emulate pflag only calling set when flag is specified on the command line + err := cmd.Flags().Set("test", tt.value) + if err != nil && !tt.wantErr { + t.Fatalf("unexpected error: %v", err) + } + if err == nil && tt.wantErr { + t.Fatalf("expected error, got none") + } + } + + got := SecretFlagToStringPointer(params.Printer, &cmd, "test") + + if got != tt.want && *got != *tt.want { + t.Fatalf("unexpected value: got %q, want %q", *got, *tt.want) + } + if tt.wantStdErr != "" { + message, err := params.Err.ReadString('\n') + if err != nil && err != io.EOF { + t.Fatalf("reading stderr: %v", err) + } + if message != tt.wantStdErr { + t.Fatalf("unexpected stderr: got %q, want %q", message, tt.wantStdErr) + } + } + }) + } +} + +func TestSecretFlag_Usage(t *testing.T) { + t.Parallel() + tests := []struct { + in string + want string + }{ + { + in: "password", + want: "Password", + }, + { + in: "Password", + want: "Password", + }, + { + in: "", + want: "", + }, + { + in: "secret-key", + want: "Secret-Key", + }, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%q -> %q", tt.in, tt.want), func(t *testing.T) { + t.Parallel() + params := testparams.NewTestParams() + flag := SecretFlag(tt.in, params.CmdParams) + got := flag.Usage() + if !strings.HasPrefix(got, tt.want) { + t.Fatalf("unexpected usage: got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/flags/string_enum.go b/internal/pkg/flags/string_enum.go new file mode 100644 index 000000000..003074650 --- /dev/null +++ b/internal/pkg/flags/string_enum.go @@ -0,0 +1,134 @@ +package flags + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" +) + +type stringEnumFlag[T ~string] struct { + ignoreCase bool + options []T + value T + defaultValue T + valueSet bool + docs string + name string + shortHand *string +} + +type StringEnumFlagOption[T ~string] func(*stringEnumFlag[T]) + +func StringEnumIgnoreCase[T ~string]() StringEnumFlagOption[T] { + return func(f *stringEnumFlag[T]) { + f.ignoreCase = true + } +} + +func StringEnumDefaultValue[T ~string](value T) StringEnumFlagOption[T] { + return func(f *stringEnumFlag[T]) { + f.value = value + f.defaultValue = value + f.valueSet = true + } +} + +func StringEnumShortHand[T ~string](short string) StringEnumFlagOption[T] { + return func(f *stringEnumFlag[T]) { + f.shortHand = &short + } +} + +func StringEnumFlag[T ~string](name string, possibleValues []T, docs string, opts ...StringEnumFlagOption[T]) *stringEnumFlag[T] { + f := &stringEnumFlag[T]{ + name: name, + docs: docs, + } + for _, v := range possibleValues { + if string(v) != "unknown_default_open_api" { + f.options = append(f.options, v) + } + } + for _, opt := range opts { + opt(f) + } + return f +} + +var _ pflag.Value = &stringEnumFlag[string]{} + +func (s *stringEnumFlag[T]) Register(fs *pflag.FlagSet) { + if s.shortHand == nil { + fs.Var(s, s.name, s.Usage()) + } else { + fs.VarP(s, s.name, *s.shortHand, s.Usage()) + } +} + +func (s *stringEnumFlag[T]) Usage() string { + return s.docs + fmt.Sprintf(" (one of: %s)", s.fmtValues(s.options)) +} + +func (s *stringEnumFlag[T]) Get() T { + return s.value +} + +func (s *stringEnumFlag[T]) Ptr() *T { + if s.valueSet { + return &s.value + } + return nil +} + +func (s *stringEnumFlag[T]) Name() string { + return s.name +} + +func (s *stringEnumFlag[T]) String() string { + return string(s.value) +} + +func (s *stringEnumFlag[T]) fmtValues(xs []T) string { + var sb strings.Builder + sb.WriteString("[") + for i, v := range xs { + sb.WriteString(string(v)) + if i != len(xs)-1 { + sb.WriteString(", ") + } + } + sb.WriteString("]") + return sb.String() +} + +func (s *stringEnumFlag[T]) Set(v string) error { + for _, o := range s.options { + if !s.ignoreCase && v == string(o) { + s.value = T(v) + s.valueSet = true + return nil + } else if s.ignoreCase && strings.EqualFold(v, string(o)) { + s.value = T(strings.ToLower(v)) + s.valueSet = true + return nil + } + } + + return fmt.Errorf("found value %q, expected one of %q", v, s.options) +} + +func (s *stringEnumFlag[T]) Type() string { + return "string" +} + +func (s *stringEnumFlag[T]) Reset() { + if s.defaultValue == "" { + var zero T + s.value = zero + s.valueSet = false + } else { + s.value = s.defaultValue + s.valueSet = true + } +} diff --git a/internal/pkg/flags/string_enum_test.go b/internal/pkg/flags/string_enum_test.go new file mode 100644 index 000000000..966c41b44 --- /dev/null +++ b/internal/pkg/flags/string_enum_test.go @@ -0,0 +1,149 @@ +package flags + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestStringEnumFlag_Set(t *testing.T) { + tests := []struct { + name string + options []string + ignoreCase bool + setValue string + want string + wantErr bool + }{ + { + name: "valid value", + options: []string{"a", "b", "c"}, + setValue: "a", + want: "a", + }, + { + name: "invalid value", + options: []string{"a", "b", "c"}, + setValue: "d", + wantErr: true, + }, + { + name: "empty value", + options: []string{"a", "b", "c"}, + setValue: "", + wantErr: true, + }, + { + name: "case sensitive mismatch", + options: []string{"A", "B"}, + setValue: "a", + ignoreCase: false, + wantErr: true, + }, + { + name: "case insensitive match", + options: []string{"A", "B"}, + setValue: "a", + ignoreCase: true, + want: "a", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := []StringEnumFlagOption[string]{} + if tt.ignoreCase { + opts = append(opts, StringEnumIgnoreCase[string]()) + } + f := StringEnumFlag("test", tt.options, "docs", opts...) + + err := f.Set(tt.setValue) + if (err != nil) != tt.wantErr { + t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + got := f.Get() + if got != tt.want { + t.Errorf("Set() got = %v, want %v", got, tt.want) + } + } + }) + } +} + +func TestStringEnumFlag_DefaultValue(t *testing.T) { + f := StringEnumFlag("test", []string{"a", "b"}, "docs", StringEnumDefaultValue("a")) + + got := f.Get() + if got != "a" { + t.Errorf("Expected default value a, got %v", got) + } + + // Setting a value should override the default + err := f.Set("b") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + got = f.Get() + if got != "b" { + t.Errorf("Expected value b after Set, got %v", got) + } +} + +func TestStringEnumFlag_Usage(t *testing.T) { + f := StringEnumFlag("test", []string{"a", "b"}, "docs") + usage := f.Usage() + if usage != "docs (one of: [a, b])" { + t.Errorf("Expected usage 'docs (possible values: [a, b])', got %q", usage) + } +} + +func TestStringEnumFlag_UnknownDefaultOpenAPI(t *testing.T) { + f := StringEnumFlag("test", []string{"a", "unknown_default_open_api", "b"}, "docs") + usage := f.Usage() + if usage != "docs (one of: [a, b])" { + t.Errorf("Expected unknown_default_open_api to be filtered out, got %q", usage) + } +} + +func TestStringEnumFlag_Register(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + f := StringEnumFlag("my-flag", []string{"a", "b"}, "docs") + f.Register(cmd.Flags()) + + flag := cmd.Flags().Lookup("my-flag") + if flag == nil { + t.Fatalf("Expected flag 'my-flag' to be registered") + } + if flag.Usage != "docs (one of: [a, b])" { + t.Errorf("Expected flag usage to be set correctly") + } +} + +func TestStringEnumFlag_Ptr(t *testing.T) { + f := StringEnumFlag("test", []string{"a", "b"}, "docs") + if f.Ptr() != nil { + t.Errorf("Expected Ptr() to be nil initially, got %v", *f.Ptr()) + } + + err := f.Set("a") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + ptr := f.Ptr() + if ptr == nil { + t.Errorf("Expected Ptr() to not be nil after Set") + } else if *ptr != "a" { + t.Errorf("Expected Ptr() to point to 'a', got %v", *ptr) + } + + fWithDefault := StringEnumFlag("test_default", []string{"a", "b"}, "docs", StringEnumDefaultValue("b")) + ptrDefault := fWithDefault.Ptr() + if ptrDefault == nil { + t.Errorf("Expected Ptr() to not be nil with default value") + } else if *ptrDefault != "b" { + t.Errorf("Expected Ptr() to point to 'b' with default value, got %v", *ptrDefault) + } +} diff --git a/internal/pkg/flags/string_enumslice.go b/internal/pkg/flags/string_enumslice.go new file mode 100644 index 000000000..22b8048c2 --- /dev/null +++ b/internal/pkg/flags/string_enumslice.go @@ -0,0 +1,144 @@ +package flags + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type stringEnumSliceFlag[T ~string] struct { + ignoreCase bool + options []T + value []T + defaultValues []T + valueSet bool + docs string + name string +} + +type StringEnumSliceFlagOption[T ~string] func(*stringEnumSliceFlag[T]) + +func IgnoreCase[T ~string]() StringEnumSliceFlagOption[T] { + return func(f *stringEnumSliceFlag[T]) { + f.ignoreCase = true + } +} + +func DefaultValues[T ~string](values ...T) StringEnumSliceFlagOption[T] { + return func(f *stringEnumSliceFlag[T]) { + f.value = append(f.value, values...) + f.defaultValues = append(f.defaultValues, values...) + } +} + +func StringEnumSliceFlag[T ~string](name string, possibleValues []T, docs string, opts ...StringEnumSliceFlagOption[T]) *stringEnumSliceFlag[T] { + f := &stringEnumSliceFlag[T]{ + name: name, + docs: docs, + } + for _, v := range possibleValues { + if string(v) != "unknown_default_open_api" { + f.options = append(f.options, v) + } + } + for _, opt := range opts { + opt(f) + } + return f +} + +var _ pflag.Value = &stringEnumSliceFlag[string]{} + +func (s *stringEnumSliceFlag[T]) Register(cmd *cobra.Command) { + cmd.Flags().Var(s, s.name, s.Usage()) +} + +func (s *stringEnumSliceFlag[T]) Usage() string { + return s.docs + fmt.Sprintf(" (multiple of: %s)", s.fmtValues(s.options)) +} + +func (s *stringEnumSliceFlag[T]) Get() []T { + return s.value +} + +func (s *stringEnumSliceFlag[T]) Ptr() *[]T { + if s.valueSet || s.value != nil { + return &s.value + } + return nil +} + +func (s *stringEnumSliceFlag[T]) Name() string { + return s.name +} + +func (s *stringEnumSliceFlag[T]) String() string { + return s.fmtValues(s.value) +} + +func (s *stringEnumSliceFlag[T]) fmtValues(xs []T) string { + var sb strings.Builder + sb.WriteString("[") + for i, v := range xs { + sb.WriteString(string(v)) + if i != len(xs)-1 { + sb.WriteString(", ") + } + } + sb.WriteString("]") + return sb.String() +} + +func (s *stringEnumSliceFlag[T]) Set(value string) error { + // If the default value is still set, remove it + // (Since we're going to append the incoming values to f.value) + if !s.valueSet { + s.value = nil + s.valueSet = true + } + + if value == "" { + return fmt.Errorf("value cannot be empty") + } + values := strings.Split(value, ",") + return s.appendToValue(values) +} + +func (s *stringEnumSliceFlag[T]) Type() string { + return "stringSlice" +} + +func (s *stringEnumSliceFlag[T]) appendToValue(values []string) error { + for _, v := range values { + v = strings.TrimSpace(v) + + foundValid := false + for _, o := range s.options { + if !s.ignoreCase && v == string(o) { + s.value = append(s.value, T(v)) + foundValid = true + break + } else if s.ignoreCase && strings.EqualFold(v, string(o)) { + s.value = append(s.value, T(strings.ToLower(v))) + foundValid = true + break + } + } + + if !foundValid { + return fmt.Errorf("found value %q, expected one of %q", v, s.options) + } + } + return nil +} + +func (s *stringEnumSliceFlag[T]) Reset() { + if s.defaultValues == nil { + s.value = nil + s.valueSet = false + } else { + s.value = append([]T{}, s.defaultValues...) + } +} diff --git a/internal/pkg/flags/string_enumslice_test.go b/internal/pkg/flags/string_enumslice_test.go new file mode 100644 index 000000000..6a04732e8 --- /dev/null +++ b/internal/pkg/flags/string_enumslice_test.go @@ -0,0 +1,161 @@ +package flags + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestStringEnumSliceFlag_Set(t *testing.T) { + tests := []struct { + name string + options []string + ignoreCase bool + setValue string + want []string + wantErr bool + }{ + { + name: "valid value", + options: []string{"a", "b", "c"}, + setValue: "a", + want: []string{"a"}, + wantErr: false, + }, + { + name: "multiple valid values", + options: []string{"a", "b", "c"}, + setValue: "a,b", + want: []string{"a", "b"}, + wantErr: false, + }, + { + name: "multiple valid values with spaces", + options: []string{"a", "b", "c"}, + setValue: "a, b ,c", + want: []string{"a", "b", "c"}, + wantErr: false, + }, + { + name: "invalid value", + options: []string{"a", "b", "c"}, + setValue: "d", + wantErr: true, + }, + { + name: "partially invalid value", + options: []string{"a", "b", "c"}, + setValue: "a,d", + wantErr: true, + }, + { + name: "empty value", + options: []string{"a", "b", "c"}, + setValue: "", + wantErr: true, + }, + { + name: "case sensitive mismatch", + options: []string{"A", "B"}, + setValue: "a", + ignoreCase: false, + wantErr: true, + }, + { + name: "case insensitive match", + options: []string{"A", "B"}, + setValue: "a", + ignoreCase: true, + want: []string{"a"}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := []StringEnumSliceFlagOption[string]{} + if tt.ignoreCase { + opts = append(opts, IgnoreCase[string]()) + } + f := StringEnumSliceFlag("test", tt.options, "docs", opts...) + + err := f.Set(tt.setValue) + if (err != nil) != tt.wantErr { + t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + got := f.Get() + if len(got) != len(tt.want) { + t.Errorf("Set() got = %v, want %v", got, tt.want) + return + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("Set() got = %v, want %v", got, tt.want) + break + } + } + } + }) + } +} + +func TestStringEnumSliceFlag_DefaultValues(t *testing.T) { + f := StringEnumSliceFlag("test", []string{"a", "b"}, "docs", DefaultValues("a")) + + got := f.Get() + if len(got) != 1 || got[0] != "a" { + t.Errorf("Expected default value [a], got %v", got) + } + + // Setting a value should override the default + err := f.Set("b") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + got = f.Get() + if len(got) != 1 || got[0] != "b" { + t.Errorf("Expected value [b] after Set, got %v", got) + } + + // Setting another value should append + err = f.Set("a") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + got = f.Get() + if len(got) != 2 || got[0] != "b" || got[1] != "a" { + t.Errorf("Expected value [b, a] after second Set, got %v", got) + } +} + +func TestStringEnumSliceFlag_Usage(t *testing.T) { + f := StringEnumSliceFlag("test", []string{"a", "b"}, "docs") + usage := f.Usage() + if usage != "docs (multiple of: [a, b])" { + t.Errorf("Expected usage 'docs (possible values: [a, b])', got %q", usage) + } +} + +func TestStringEnumSliceFlag_UnknownDefaultOpenAPI(t *testing.T) { + f := StringEnumSliceFlag("test", []string{"a", "unknown_default_open_api", "b"}, "docs") + usage := f.Usage() + if usage != "docs (multiple of: [a, b])" { + t.Errorf("Expected unknown_default_open_api to be filtered out, got %q", usage) + } +} + +func TestStringEnumSliceFlag_Register(t *testing.T) { + cmd := &cobra.Command{Use: "test"} + f := StringEnumSliceFlag("my-flag", []string{"a", "b"}, "docs") + f.Register(cmd) + + flag := cmd.Flags().Lookup("my-flag") + if flag == nil { + t.Fatalf("Expected flag 'my-flag' to be registered") + } + if flag.Usage != "docs (multiple of: [a, b])" { + t.Errorf("Expected flag usage to be set correctly") + } +} diff --git a/internal/pkg/generic-client/generic_client.go b/internal/pkg/generic-client/generic_client.go new file mode 100644 index 000000000..ba5185dcb --- /dev/null +++ b/internal/pkg/generic-client/generic_client.go @@ -0,0 +1,51 @@ +package genericclient + +import ( + "github.com/spf13/viper" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type CreateApiClient[T any] func(opts ...sdkConfig.ConfigurationOption) (T, error) + +// ConfigureClientGeneric contains the generic code which needs to be executed in order to configure the api client. +func ConfigureClientGeneric[T any](p *print.Printer, cliVersion, customEndpoint string, useRegion bool, createApiClient CreateApiClient[T]) (T, error) { + // return value if an error happens + var zero T + authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) + if err != nil { + p.Debug(print.ErrorLevel, "configure authentication: %v", err) + return zero, &errors.AuthError{} + } + cfgOptions := []sdkConfig.ConfigurationOption{ + utils.UserAgentConfigOption(cliVersion), + authCfgOption, + } + + if customEndpoint != "" { + cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) + } + + if useRegion { + cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion(viper.GetString(config.RegionKey))) + } + + if p.IsVerbosityDebug() { + cfgOptions = append(cfgOptions, + sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), + ) + } + + apiClient, err := createApiClient(cfgOptions...) + if err != nil { + p.Debug(print.ErrorLevel, "create new API client: %v", err) + return zero, &errors.AuthError{} + } + + return apiClient, nil +} diff --git a/internal/pkg/globalflags/global_flags.go b/internal/pkg/globalflags/global_flags.go index 9f53ec4f7..0f08767f0 100644 --- a/internal/pkg/globalflags/global_flags.go +++ b/internal/pkg/globalflags/global_flags.go @@ -13,12 +13,10 @@ import ( ) const ( - AsyncFlag = "async" - AssumeYesFlag = "assume-yes" - OutputFormatFlag = "output-format" - ProjectIdFlag = "project-id" - RegionFlag = "region" - VerbosityFlag = "verbosity" + AsyncFlag = "async" + AssumeYesFlag = "assume-yes" + ProjectIdFlag = "project-id" + RegionFlag = "region" DebugVerbosity = string(print.DebugLevel) InfoVerbosity = string(print.InfoLevel) @@ -28,8 +26,22 @@ const ( VerbosityDefault = InfoVerbosity ) -var outputFormatFlagOptions = []string{print.JSONOutputFormat, print.PrettyOutputFormat, print.NoneOutputFormat, print.YAMLOutputFormat} -var verbosityFlagOptions = []string{DebugVerbosity, InfoVerbosity, WarningVerbosity, ErrorVerbosity} +var ( + OutputFormatFlag = flags.StringEnumFlag( + "output-format", + []string{print.JSONOutputFormat, print.PrettyOutputFormat, print.NoneOutputFormat, print.YAMLOutputFormat}, + "Output format,", + flags.StringEnumIgnoreCase[string](), + flags.StringEnumShortHand[string]("o"), + ) + VerbosityFlag = flags.StringEnumFlag( + "verbosity", + []string{DebugVerbosity, InfoVerbosity, WarningVerbosity, ErrorVerbosity}, + "Verbosity of the CLI,", + flags.StringEnumDefaultValue(VerbosityDefault), + flags.StringEnumIgnoreCase[string](), + ) +) type GlobalFlagModel struct { Async bool @@ -47,10 +59,10 @@ func Configure(flagSet *pflag.FlagSet) error { return fmt.Errorf("bind --%s flag to config: %w", ProjectIdFlag, err) } - flagSet.VarP(flags.EnumFlag(true, "", outputFormatFlagOptions...), OutputFormatFlag, "o", fmt.Sprintf("Output format, one of %q", outputFormatFlagOptions)) - err = viper.BindPFlag(config.OutputFormatKey, flagSet.Lookup(OutputFormatFlag)) + OutputFormatFlag.Register(flagSet) + err = viper.BindPFlag(config.OutputFormatKey, flagSet.Lookup(OutputFormatFlag.Name())) if err != nil { - return fmt.Errorf("bind --%s flag to config: %w", OutputFormatFlag, err) + return fmt.Errorf("bind --%s flag to config: %w", OutputFormatFlag.Name(), err) } flagSet.Bool(AsyncFlag, false, "If set, runs the command asynchronously") @@ -60,11 +72,15 @@ func Configure(flagSet *pflag.FlagSet) error { } flagSet.BoolP(AssumeYesFlag, "y", false, "If set, skips all confirmation prompts") + err = viper.BindPFlag(config.AssumeYesKey, flagSet.Lookup(AssumeYesFlag)) + if err != nil { + return fmt.Errorf("bind --%s flag to config: %w", AssumeYesFlag, err) + } - flagSet.Var(flags.EnumFlag(true, VerbosityDefault, verbosityFlagOptions...), VerbosityFlag, fmt.Sprintf("Verbosity of the CLI, one of %q", verbosityFlagOptions)) - err = viper.BindPFlag(config.VerbosityKey, flagSet.Lookup(VerbosityFlag)) + VerbosityFlag.Register(flagSet) + err = viper.BindPFlag(config.VerbosityKey, flagSet.Lookup(VerbosityFlag.Name())) if err != nil { - return fmt.Errorf("bind --%s flag to config: %w", VerbosityFlag, err) + return fmt.Errorf("bind --%s flag to config: %w", VerbosityFlag.Name(), err) } flagSet.String(RegionFlag, "", "Target region for region-specific requests") @@ -76,10 +92,10 @@ func Configure(flagSet *pflag.FlagSet) error { return nil } -func Parse(p *print.Printer, cmd *cobra.Command) *GlobalFlagModel { +func Parse(_ *print.Printer, _ *cobra.Command) *GlobalFlagModel { return &GlobalFlagModel{ Async: viper.GetBool(config.AsyncKey), - AssumeYes: flags.FlagToBoolValue(p, cmd, AssumeYesFlag), + AssumeYes: viper.GetBool(config.AssumeYesKey), OutputFormat: viper.GetString(config.OutputFormatKey), ProjectId: viper.GetString(config.ProjectIdKey), Region: viper.GetString(config.RegionKey), diff --git a/internal/pkg/print/debug.go b/internal/pkg/print/debug.go index 60962ba7b..793c54bd3 100644 --- a/internal/pkg/print/debug.go +++ b/internal/pkg/print/debug.go @@ -16,11 +16,11 @@ import ( var defaultHTTPHeaders = []string{"Accept", "Content-Type", "Content-Length", "User-Agent", "Date", "Referrer-Policy", "Traceparent"} -// BuildDebugStrFromInputModel converts an input model to a user-friendly string representation. +// buildDebugStrFromInputModel converts an input model to a user-friendly string representation. // This function converts the input model to a map, removes empty values, and generates a string representation of the map. // The purpose of this function is to provide a more readable output than the default JSON representation. // It is particularly useful when outputting to the slog logger, as the JSON format with escaped quotes does not look good. -func BuildDebugStrFromInputModel(model any) (string, error) { +func buildDebugStrFromInputModel(model any) (string, error) { // Marshaling and Unmarshaling is the best way to convert the struct to a map modelBytes, err := json.Marshal(model) if err != nil { diff --git a/internal/pkg/print/debug_test.go b/internal/pkg/print/debug_test.go index 45ef90482..abc3dedeb 100644 --- a/internal/pkg/print/debug_test.go +++ b/internal/pkg/print/debug_test.go @@ -171,7 +171,7 @@ func TestBuildDebugStrFromInputModel(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { model := tt.model - actual, err := BuildDebugStrFromInputModel(model) + actual, err := buildDebugStrFromInputModel(model) if err != nil { if !tt.isValid { return diff --git a/internal/pkg/print/print.go b/internal/pkg/print/print.go index 63f48fe89..d250296df 100644 --- a/internal/pkg/print/print.go +++ b/internal/pkg/print/print.go @@ -2,19 +2,21 @@ package print import ( "bufio" + "bytes" + "encoding/json" "errors" "fmt" - "syscall" - + "io" "log/slog" + "math" "os" "os/exec" "strings" "github.com/fatih/color" + "github.com/goccy/go-yaml" "github.com/lmittmann/tint" "github.com/mattn/go-colorable" - "github.com/spf13/cobra" "github.com/spf13/viper" "golang.org/x/term" @@ -47,19 +49,32 @@ var ( ) type Printer struct { - Cmd *cobra.Command + AssumeYes bool Verbosity Level + StdIn io.Reader + StdOut io.Writer + StdErr io.Writer + ErrPrefix string } -// Creates a new printer, including setting up the default logger. -func NewPrinter() *Printer { - w := os.Stderr +// NewPrinter creates a new printer, including setting up the default logger. +func NewPrinter(stdIn io.Reader, stdOut, stdErr io.Writer) *Printer { + logW := stdErr + if f, ok := stdErr.(*os.File); ok { + logW = colorable.NewColorable(f) + } logger := slog.New( - tint.NewHandler(colorable.NewColorable(w), &tint.Options{AddSource: false, Level: slog.LevelDebug}), + tint.NewHandler(logW, &tint.Options{AddSource: false, Level: slog.LevelDebug}), ) slog.SetDefault(logger) - return &Printer{} + return &Printer{ + StdIn: stdIn, + StdOut: stdOut, + StdErr: stdErr, + ErrPrefix: "Error:", + Verbosity: InfoLevel, + } } // Print an output using Printf to the defined output (falling back to Stderr if not set). @@ -69,7 +84,7 @@ func (p *Printer) Outputf(msg string, args ...any) { if outputFormat == NoneOutputFormat { return } - p.Cmd.Printf(msg, args...) + mustPrint(fmt.Fprintf(p.StdOut, msg, args...)) } // Print an output using Println to the defined output (falling back to Stderr if not set). @@ -79,7 +94,7 @@ func (p *Printer) Outputln(msg string) { if outputFormat == NoneOutputFormat { return } - p.Cmd.Println(msg) + mustPrint(fmt.Fprintln(p.StdOut, msg)) } // Print a Debug level log through the "slog" package. @@ -107,7 +122,7 @@ func (p *Printer) Info(msg string, args ...any) { if !p.IsVerbosityDebug() && !p.IsVerbosityInfo() { return } - p.Cmd.PrintErrf(msg, args...) + mustPrint(fmt.Fprintf(p.StdErr, msg, args...)) } // Print a Warn level output to the defined Err output (falling back to Stderr if not set). @@ -117,13 +132,13 @@ func (p *Printer) Warn(msg string, args ...any) { return } warning := fmt.Sprintf(msg, args...) - p.Cmd.PrintErrf("%s %s", YellowBold("Warning:"), warning) + mustPrint(fmt.Fprintf(p.StdErr, "%s %s", YellowBold("Warning:"), warning)) } // Print an Error level output to the defined Err output (falling back to Stderr if not set). func (p *Printer) Error(msg string, args ...any) { err := fmt.Sprintf(msg, args...) - p.Cmd.PrintErrln(RedBold(p.Cmd.ErrPrefix()), err) + mustPrint(fmt.Fprintln(p.StdErr, RedBold(p.ErrPrefix), err)) } // Prompts the user for confirmation. @@ -131,10 +146,14 @@ func (p *Printer) Error(msg string, args ...any) { // Returns nil only if the user (explicitly) answers positive. // Returns ErrAborted if the user answers negative. func (p *Printer) PromptForConfirmation(prompt string) error { + if p.AssumeYes { + p.Warn("Auto-confirming prompt: %q\n", prompt) + return nil + } question := fmt.Sprintf("%s [y/N] ", prompt) - reader := bufio.NewReader(p.Cmd.InOrStdin()) + reader := bufio.NewReader(p.StdIn) for i := 0; i < 3; i++ { - p.Cmd.PrintErr(question) + mustPrint(fmt.Fprint(p.StdErr, question)) answer, err := reader.ReadString('\n') if err != nil { continue @@ -154,8 +173,12 @@ func (p *Printer) PromptForConfirmation(prompt string) error { // // Returns nil if the user presses Enter. func (p *Printer) PromptForEnter(prompt string) error { - reader := bufio.NewReader(p.Cmd.InOrStdin()) - p.Cmd.PrintErr(prompt) + if p.AssumeYes { + p.Warn("Auto-confirming prompt: %q", prompt) + return nil + } + reader := bufio.NewReader(p.StdIn) + mustPrint(fmt.Fprint(p.StdErr, prompt)) _, err := reader.ReadString('\n') if err != nil { return fmt.Errorf("read user response: %w", err) @@ -167,13 +190,31 @@ func (p *Printer) PromptForEnter(prompt string) error { // // Returns the password that was given, otherwise returns error func (p *Printer) PromptForPassword(prompt string) (string, error) { - p.Cmd.PrintErr(prompt) + mustPrint(fmt.Fprint(p.StdErr, prompt)) defer p.Outputln("") - bytePassword, err := term.ReadPassword(int(syscall.Stdin)) + + if f, ok := p.StdIn.(*os.File); ok { + uint_fd := f.Fd() + if uint_fd > math.MaxInt { + return "", fmt.Errorf("uint_fd is too large") + } + fd := int(uint_fd) + if term.IsTerminal(fd) { + bytePassword, err := term.ReadPassword(fd) + if err != nil { + return "", fmt.Errorf("read password: %w", err) + } + return string(bytePassword), nil + } + } + + // Fallback for non-terminal environments + reader := bufio.NewReader(p.StdIn) + pw, err := reader.ReadString('\n') if err != nil { - return "", fmt.Errorf("read password: %w", err) + return "", fmt.Errorf("read password from non-terminal: %w", err) } - return string(bytePassword), nil + return pw[:len(pw)-1], nil // remove trailing newline } // Shows the content in the command's stdout using the "less" command @@ -198,7 +239,7 @@ func (p *Printer) PagerDisplay(content string) error { } pagerCmd.Stdin = strings.NewReader(content) - pagerCmd.Stdout = p.Cmd.OutOrStdout() + pagerCmd.Stdout = p.StdOut p.Debug(DebugLevel, "using pager: %s", pagerCmd.Args[0]) err := pagerCmd.Run() @@ -228,3 +269,49 @@ func (p *Printer) IsVerbosityWarning() bool { func (p *Printer) IsVerbosityError() bool { return p.Verbosity == ErrorLevel } + +// DebugInputModel prints the given input model in case verbosity level is set to Debug, does nothing otherwise +func (p *Printer) DebugInputModel(model any) { + if p.IsVerbosityDebug() { + modelStr, err := buildDebugStrFromInputModel(model) + if err != nil { + p.Debug(ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(DebugLevel, "parsed input values: %s", modelStr) + } + } +} + +func (p *Printer) OutputResult(outputFormat string, output any, prettyOutputFunc func() error) error { + switch outputFormat { + case JSONOutputFormat: + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + err := encoder.Encode(output) + if err != nil { + return fmt.Errorf("marshal json: %w", err) + } + details := buffer.Bytes() + p.Outputln(string(details)) + + return nil + case YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true), yaml.UseJSONMarshaler()) + if err != nil { + return fmt.Errorf("marshal yaml: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + return prettyOutputFunc() + } +} + +func mustPrint(_ int, err error) { + if err != nil { + panic(err) + } +} diff --git a/internal/pkg/print/print_test.go b/internal/pkg/print/print_test.go index 5c68dc2cf..6583769fa 100644 --- a/internal/pkg/print/print_test.go +++ b/internal/pkg/print/print_test.go @@ -6,9 +6,9 @@ import ( "fmt" "io" "log/slog" + "sync" "testing" - "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -56,11 +56,9 @@ func TestOutputf(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, } viper.Reset() @@ -128,11 +126,9 @@ func TestOutputln(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, } viper.Reset() @@ -193,11 +189,9 @@ func TestPagerDisplay(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, } viper.Reset() @@ -293,13 +287,11 @@ func TestDebug(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{AddSource: true, Level: slog.LevelDebug})) slog.SetDefault(logger) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, } @@ -357,11 +349,9 @@ func TestInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, } @@ -417,11 +407,9 @@ func TestWarn(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, } @@ -477,12 +465,11 @@ func TestError(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var buf bytes.Buffer - cmd := &cobra.Command{} - cmd.SetOut(&buf) - cmd.SetErr(&buf) p := &Printer{ - Cmd: cmd, + StdOut: &buf, + StdErr: &buf, Verbosity: tt.verbosity, + ErrPrefix: "Error:", } p.Error("%s", tt.message) @@ -509,6 +496,7 @@ func TestPromptForConfirmation(t *testing.T) { verbosity Level isValid bool isAborted bool + assumeYes bool }{ // Note: Some of these inputs have normal spaces, others have tabs { @@ -647,6 +635,13 @@ func TestPromptForConfirmation(t *testing.T) { verbosity: DebugLevel, isValid: false, }, + { + description: "no input with assume yes", + input: "", + verbosity: DebugLevel, + isValid: true, + assumeYes: true, + }, } for _, tt := range tests { @@ -657,14 +652,12 @@ func TestPromptForConfirmation(t *testing.T) { t.Fatalf("failed to initialize mock input: %v", err) } - cmd := &cobra.Command{} - cmd.SetOut(io.Discard) // Suppresses console prints - cmd.SetErr(io.Discard) - cmd.SetIn(buffer) - p := &Printer{ - Cmd: cmd, + StdIn: buffer, + StdOut: io.Discard, + StdErr: io.Discard, Verbosity: tt.verbosity, + AssumeYes: tt.assumeYes, } err = p.PromptForConfirmation("") @@ -714,9 +707,7 @@ func TestIsVerbosityDebug(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} p := &Printer{ - Cmd: cmd, Verbosity: tt.verbosity, } @@ -758,9 +749,7 @@ func TestIsVerbosityInfo(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} p := &Printer{ - Cmd: cmd, Verbosity: tt.verbosity, } @@ -802,9 +791,7 @@ func TestIsVerbosityWarning(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} p := &Printer{ - Cmd: cmd, Verbosity: tt.verbosity, } @@ -846,9 +833,7 @@ func TestIsVerbosityError(t *testing.T) { } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - cmd := &cobra.Command{} p := &Printer{ - Cmd: cmd, Verbosity: tt.verbosity, } @@ -860,3 +845,127 @@ func TestIsVerbosityError(t *testing.T) { }) } } + +func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + output any + prettyOutputFunc func() error + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "output format is JSON", + args: args{ + outputFormat: JSONOutputFormat, + output: struct{}{}, + }, + }, + { + name: "output format is JSON and output is nil", + args: args{ + outputFormat: JSONOutputFormat, + output: nil, + }, + }, + { + name: "output format is YAML", + args: args{ + outputFormat: YAMLOutputFormat, + output: struct{}{}, + }, + }, + { + name: "output format is YAML and output is nil", + args: args{ + outputFormat: YAMLOutputFormat, + output: nil, + }, + }, + { + name: "should return error of pretty output func", + args: args{ + outputFormat: PrettyOutputFormat, + output: struct{}{}, + prettyOutputFunc: func() error { + return fmt.Errorf("dummy error") + }, + }, + wantErr: true, + }, + { + name: "success of pretty output func", + args: args{ + outputFormat: PrettyOutputFormat, + output: struct{}{}, + prettyOutputFunc: func() error { + return nil + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buffer bytes.Buffer + p := &Printer{ + StdOut: &buffer, + Verbosity: ErrorLevel, + } + + if err := p.OutputResult(tt.args.outputFormat, tt.args.output, tt.args.prettyOutputFunc); (err != nil) != tt.wantErr { + t.Errorf("OutputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestPromptForPassword(t *testing.T) { + tests := []struct { + description string + input string + }{ + { + description: "password", + input: "mypassword\n", + }, + { + description: "empty password", + input: "\n", + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + r, w := io.Pipe() + defer func() { + r.Close() //nolint:errcheck // ignore error on close + w.Close() //nolint:errcheck // ignore error on close + }() + p := &Printer{ + StdIn: r, + StdErr: &bytes.Buffer{}, + StdOut: &bytes.Buffer{}, + Verbosity: ErrorLevel, + } + var pw string + var err error + var wg sync.WaitGroup + wg.Add(1) + go func() { + pw, err = p.PromptForPassword("Enter password: ") + wg.Done() + }() + w.Write([]byte(tt.input)) //nolint:errcheck // ignore error + wg.Wait() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + withoutNewline := tt.input[:len(tt.input)-1] + if pw != withoutNewline { + t.Fatalf("unexpected password: got %q, want %q", pw, withoutNewline) + } + }) + } +} diff --git a/internal/pkg/projectname/project_name.go b/internal/pkg/projectname/project_name.go index 84381a8c6..232255d29 100644 --- a/internal/pkg/projectname/project_name.go +++ b/internal/pkg/projectname/project_name.go @@ -35,14 +35,14 @@ func GetProjectName(ctx context.Context, p *print.Printer, cliVersion string, cm return "", fmt.Errorf("configure resource manager client: %w", err) } - projectName, err := utils.GetProjectName(ctx, apiClient, projectId) + projectName, err := utils.GetProjectName(ctx, apiClient.DefaultAPI, projectId) if err != nil { return "", fmt.Errorf("get project name: %w", err) } // If project ID is set in config, we store the project name in config // (So next time we can just pull it from there) - if !(isProjectIdSetInFlags(p, cmd) || isProjectIdSetInEnvVar()) { + if !isProjectIdSetInFlags(p, cmd) && !isProjectIdSetInEnvVar() { viper.Set(config.ProjectNameKey, projectName) err = config.Write() if err != nil { @@ -61,10 +61,7 @@ func useProjectNameFromConfig(p *print.Printer, cmd *cobra.Command) bool { projectIdSetInFlags := isProjectIdSetInFlags(p, cmd) projectIdSetInEnv := isProjectIdSetInEnvVar() projectName := viper.GetString(config.ProjectNameKey) - projectNameSet := false - if projectName != "" { - projectNameSet = true - } + projectNameSet := projectName != "" return !projectIdSetInFlags && !projectIdSetInEnv && projectNameSet } @@ -73,10 +70,7 @@ func isProjectIdSetInFlags(p *print.Printer, cmd *cobra.Command) bool { // viper.GetString uses the flags, and fallsback to config file // To check if projectId was passed, we use the first rather than the second projectIdFromFlag := flags.FlagToStringPointer(p, cmd, globalflags.ProjectIdFlag) - projectIdSetInFlag := false - if projectIdFromFlag != nil { - projectIdSetInFlag = true - } + projectIdSetInFlag := projectIdFromFlag != nil return projectIdSetInFlag } diff --git a/internal/pkg/projectname/project_name_test.go b/internal/pkg/projectname/project_name_test.go index ae6d79251..4409f2b38 100644 --- a/internal/pkg/projectname/project_name_test.go +++ b/internal/pkg/projectname/project_name_test.go @@ -7,8 +7,10 @@ import ( "github.com/google/uuid" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) var testProjectId = uuid.NewString() @@ -39,10 +41,10 @@ func TestGetProjectName(t *testing.T) { viper.Set(config.ProjectNameKey, tt.projectName) viper.Set(config.ProjectIdKey, tt.projectId) defer viper.Reset() - p := print.NewPrinter() + params := testparams.NewTestParams() cmd := &cobra.Command{} - projectName, err := GetProjectName(context.Background(), p, "0.0.0-dummy", cmd) + projectName, err := GetProjectName(context.Background(), params.Printer, "0.0.0-dummy", cmd) if err != nil { if tt.isValid { t.Fatalf("unexpected error: %v", err) diff --git a/internal/pkg/services/alb/client/client.go b/internal/pkg/services/alb/client/client.go index 866a76ad5..125e45415 100644 --- a/internal/pkg/services/alb/client/client.go +++ b/internal/pkg/services/alb/client/client.go @@ -1,44 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/alb" + alb "github.com/stackitcloud/stackit-sdk-go/services/alb/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*alb.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.IaaSCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := alb.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AlbCustomEndpoint), false, genericclient.CreateApiClient[*alb.APIClient](alb.NewAPIClient)) } diff --git a/internal/pkg/services/authorization/client/client.go b/internal/pkg/services/authorization/client/client.go index 7deb26b12..8646a8120 100644 --- a/internal/pkg/services/authorization/client/client.go +++ b/internal/pkg/services/authorization/client/client.go @@ -1,45 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/authorization" ) func ConfigureClient(p *print.Printer, cliVersion string) (*authorization.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.AuthorizationCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := authorization.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.AuthorizationCustomEndpointKey), false, genericclient.CreateApiClient[*authorization.APIClient](authorization.NewAPIClient)) } diff --git a/internal/pkg/services/cdn/client/client.go b/internal/pkg/services/cdn/client/client.go new file mode 100644 index 000000000..ba2f98eef --- /dev/null +++ b/internal/pkg/services/cdn/client/client.go @@ -0,0 +1,14 @@ +package client + +import ( + "github.com/spf13/viper" + cdn "github.com/stackitcloud/stackit-sdk-go/services/cdn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*cdn.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.CDNCustomEndpointKey), false, cdn.NewAPIClient) +} diff --git a/internal/pkg/services/cdn/utils/utils.go b/internal/pkg/services/cdn/utils/utils.go new file mode 100644 index 000000000..f9b903420 --- /dev/null +++ b/internal/pkg/services/cdn/utils/utils.go @@ -0,0 +1,40 @@ +package utils + +import ( + "strings" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +func ParseGeofencing(p *print.Printer, geofencingInput []string) *map[string][]string { //nolint:gocritic // convenient for setting the SDK payload + geofencing := make(map[string][]string) + for _, in := range geofencingInput { + firstSpace := strings.IndexRune(in, ' ') + if firstSpace == -1 { + p.Debug(print.ErrorLevel, "invalid geofencing entry (no space found): %q", in) + continue + } + urlPart := in[:firstSpace] + countriesPart := in[firstSpace+1:] + geofencing[urlPart] = nil + countries := strings.Split(countriesPart, ",") + for _, country := range countries { + country = strings.TrimSpace(country) + geofencing[urlPart] = append(geofencing[urlPart], country) + } + } + return &geofencing +} + +func ParseOriginRequestHeaders(p *print.Printer, originRequestHeadersInput []string) *map[string]string { //nolint:gocritic // convenient for setting the SDK payload + originRequestHeaders := make(map[string]string) + for _, in := range originRequestHeadersInput { + parts := strings.Split(in, ":") + if len(parts) != 2 { + p.Debug(print.ErrorLevel, "invalid origin request header entry (no colon found): %q", in) + continue + } + originRequestHeaders[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + return &originRequestHeaders +} diff --git a/internal/pkg/services/cdn/utils/utils_test.go b/internal/pkg/services/cdn/utils/utils_test.go new file mode 100644 index 000000000..96364289e --- /dev/null +++ b/internal/pkg/services/cdn/utils/utils_test.go @@ -0,0 +1,94 @@ +package utils + +import ( + "reflect" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" +) + +func TestParseGeofencing(t *testing.T) { + tests := []struct { + name string + input []string + want map[string][]string + }{ + { + name: "empty input", + input: nil, + want: map[string][]string{}, + }, + { + name: "single entry", + input: []string{ + "https://example.com US,CA,MX", + }, + want: map[string][]string{ + "https://example.com": {"US", "CA", "MX"}, + }, + }, + { + name: "multiple entries", + input: []string{ + "https://example.com US,CA,MX", + "https://another.com DE,FR", + }, + want: map[string][]string{ + "https://example.com": {"US", "CA", "MX"}, + "https://another.com": {"DE", "FR"}, + }, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseGeofencing(params.Printer, tt.input) + if !reflect.DeepEqual(got, &tt.want) { + t.Errorf("ParseGeofencing() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseOriginRequestHeaders(t *testing.T) { + tests := []struct { + name string + input []string + want map[string]string + }{ + { + name: "empty input", + input: nil, + want: map[string]string{}, + }, + { + name: "single entry", + input: []string{ + "X-Custom-Header: Value1", + }, + want: map[string]string{ + "X-Custom-Header": "Value1", + }, + }, + { + name: "multiple entries", + input: []string{ + "X-Custom-Header1: Value1", + "X-Custom-Header2: Value2", + }, + want: map[string]string{ + "X-Custom-Header1": "Value1", + "X-Custom-Header2": "Value2", + }, + }, + } + params := testparams.NewTestParams() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseOriginRequestHeaders(params.Printer, tt.input) + if !reflect.DeepEqual(got, &tt.want) { + t.Errorf("ParseOriginRequestHeaders() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/services/dns/client/client.go b/internal/pkg/services/dns/client/client.go index 15b824da3..7e101b00a 100644 --- a/internal/pkg/services/dns/client/client.go +++ b/internal/pkg/services/dns/client/client.go @@ -1,46 +1,15 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/dns" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*dns.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.DNSCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := dns.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.DNSCustomEndpointKey), false, genericclient.CreateApiClient[*dns.APIClient](dns.NewAPIClient)) } diff --git a/internal/pkg/services/dns/utils/utils.go b/internal/pkg/services/dns/utils/utils.go index 141fb50e0..f58f9c453 100644 --- a/internal/pkg/services/dns/utils/utils.go +++ b/internal/pkg/services/dns/utils/utils.go @@ -5,37 +5,33 @@ import ( "fmt" "math" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) -type DNSClient interface { - GetZoneExecute(ctx context.Context, projectId, zoneId string) (*dns.ZoneResponse, error) - GetRecordSetExecute(ctx context.Context, projectId, zoneId, recordSetId string) (*dns.RecordSetResponse, error) -} - -func GetZoneName(ctx context.Context, apiClient DNSClient, projectId, zoneId string) (string, error) { - resp, err := apiClient.GetZoneExecute(ctx, projectId, zoneId) +func GetZoneName(ctx context.Context, apiClient dns.DefaultAPI, projectId, zoneId string) (string, error) { + resp, err := apiClient.GetZone(ctx, projectId, zoneId).Execute() if err != nil { return "", fmt.Errorf("get DNS zone: %w", err) } - return *resp.Zone.Name, nil + return resp.Zone.Name, nil } -func GetRecordSetName(ctx context.Context, apiClient DNSClient, projectId, zoneId, recordSetId string) (string, error) { - resp, err := apiClient.GetRecordSetExecute(ctx, projectId, zoneId, recordSetId) +func GetRecordSetName(ctx context.Context, apiClient dns.DefaultAPI, projectId, zoneId, recordSetId string) (string, error) { + resp, err := apiClient.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() if err != nil { return "", fmt.Errorf("get DNS recordset: %w", err) } - return *resp.Rrset.Name, nil + return resp.Rrset.Name, nil } -func GetRecordSetType(ctx context.Context, apiClient DNSClient, projectId, zoneId, recordSetId string) (*string, error) { - resp, err := apiClient.GetRecordSetExecute(ctx, projectId, zoneId, recordSetId) +func GetRecordSetType(ctx context.Context, apiClient dns.DefaultAPI, projectId, zoneId, recordSetId string) (*string, error) { + resp, err := apiClient.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute() if err != nil { return utils.Ptr(""), fmt.Errorf("get DNS recordset: %w", err) } - return (*string)(resp.Rrset.Type), nil + return utils.Ptr(string(resp.Rrset.Type)), nil } func FormatTxtRecord(input string) (string, error) { diff --git a/internal/pkg/services/dns/utils/utils_test.go b/internal/pkg/services/dns/utils/utils_test.go index 12cae8fdc..0330ce992 100644 --- a/internal/pkg/services/dns/utils/utils_test.go +++ b/internal/pkg/services/dns/utils/utils_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/google/uuid" + dns "github.com/stackitcloud/stackit-sdk-go/services/dns/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/dns" ) var ( @@ -27,25 +28,30 @@ const ( testRecordSetType = "A" ) -type dnsClientMocked struct { +type mockSettings struct { getZoneFails bool getZoneResp *dns.ZoneResponse getRecordSetFails bool getRecordSetResp *dns.RecordSetResponse } -func (m *dnsClientMocked) GetZoneExecute(_ context.Context, _, _ string) (*dns.ZoneResponse, error) { - if m.getZoneFails { - return nil, fmt.Errorf("could not get zone") - } - return m.getZoneResp, nil -} +func newAPIMock(settings *mockSettings) dns.DefaultAPI { + return &dns.DefaultAPIServiceMock{ + GetZoneExecuteMock: utils.Ptr(func(_ dns.ApiGetZoneRequest) (*dns.ZoneResponse, error) { + if settings.getZoneFails { + return nil, fmt.Errorf("could not get zone") + } -func (m *dnsClientMocked) GetRecordSetExecute(_ context.Context, _, _, _ string) (*dns.RecordSetResponse, error) { - if m.getRecordSetFails { - return nil, fmt.Errorf("could not get record set") + return settings.getZoneResp, nil + }), + GetRecordSetExecuteMock: utils.Ptr(func(_ dns.ApiGetRecordSetRequest) (*dns.RecordSetResponse, error) { + if settings.getRecordSetFails { + return nil, fmt.Errorf("could not get record set") + } + + return settings.getRecordSetResp, nil + }), } - return m.getRecordSetResp, nil } func TestGetZoneName(t *testing.T) { @@ -59,8 +65,8 @@ func TestGetZoneName(t *testing.T) { { description: "base", getZoneResp: &dns.ZoneResponse{ - Zone: &dns.Zone{ - Name: utils.Ptr(testZoneName), + Zone: dns.Zone{ + Name: testZoneName, }, }, isValid: true, @@ -75,10 +81,10 @@ func TestGetZoneName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &dnsClientMocked{ + client := newAPIMock(&mockSettings{ getZoneFails: tt.getZoneFails, getZoneResp: tt.getZoneResp, - } + }) output, err := GetZoneName(context.Background(), client, testProjectId, testZoneId) @@ -109,8 +115,8 @@ func TestGetRecordSetName(t *testing.T) { { description: "base", getRecordSetResp: &dns.RecordSetResponse{ - Rrset: &dns.RecordSet{ - Name: utils.Ptr(testRecordSetName), + Rrset: dns.RecordSet{ + Name: testRecordSetName, }, }, isValid: true, @@ -125,10 +131,10 @@ func TestGetRecordSetName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &dnsClientMocked{ + client := newAPIMock(&mockSettings{ getRecordSetFails: tt.getRecordSetFails, getRecordSetResp: tt.getRecordSetResp, - } + }) output, err := GetRecordSetName(context.Background(), client, testProjectId, testZoneId, testRecordSetId) @@ -159,8 +165,8 @@ func TestGetRecordSetType(t *testing.T) { { description: "base", getRecordSetResp: &dns.RecordSetResponse{ - Rrset: &dns.RecordSet{ - Name: utils.Ptr(testRecordSetType), + Rrset: dns.RecordSet{ + Name: testRecordSetType, }, }, isValid: true, @@ -175,10 +181,10 @@ func TestGetRecordSetType(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &dnsClientMocked{ + client := newAPIMock(&mockSettings{ getRecordSetFails: tt.getRecordSetFails, getRecordSetResp: tt.getRecordSetResp, - } + }) output, err := GetRecordSetName(context.Background(), client, testProjectId, testZoneId, testRecordSetId) diff --git a/internal/pkg/services/edge/client/client.go b/internal/pkg/services/edge/client/client.go new file mode 100644 index 000000000..77d2677ea --- /dev/null +++ b/internal/pkg/services/edge/client/client.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package client + +import ( + "context" + + "github.com/spf13/viper" + "github.com/stackitcloud/stackit-sdk-go/services/edge" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +// APIClient is an interface that consolidates all client functionality to allow for mocking of the API client during testing. +type APIClient interface { + CreateInstance(ctx context.Context, projectId, regionId string) edge.ApiCreateInstanceRequest + DeleteInstance(ctx context.Context, projectId, regionId, instanceId string) edge.ApiDeleteInstanceRequest + DeleteInstanceByName(ctx context.Context, projectId, regionId, displayName string) edge.ApiDeleteInstanceByNameRequest + GetInstance(ctx context.Context, projectId, regionId, instanceId string) edge.ApiGetInstanceRequest + GetInstanceByName(ctx context.Context, projectId, regionId, displayName string) edge.ApiGetInstanceByNameRequest + ListInstances(ctx context.Context, projectId, regionId string) edge.ApiListInstancesRequest + UpdateInstance(ctx context.Context, projectId, regionId, instanceId string) edge.ApiUpdateInstanceRequest + UpdateInstanceByName(ctx context.Context, projectId, regionId, displayName string) edge.ApiUpdateInstanceByNameRequest + GetKubeconfigByInstanceId(ctx context.Context, projectId, regionId, instanceId string) edge.ApiGetKubeconfigByInstanceIdRequest + GetKubeconfigByInstanceName(ctx context.Context, projectId, regionId, displayName string) edge.ApiGetKubeconfigByInstanceNameRequest + GetTokenByInstanceId(ctx context.Context, projectId, regionId, instanceId string) edge.ApiGetTokenByInstanceIdRequest + GetTokenByInstanceName(ctx context.Context, projectId, regionId, displayName string) edge.ApiGetTokenByInstanceNameRequest + ListPlansProject(ctx context.Context, projectId string) edge.ApiListPlansProjectRequest +} + +// ConfigureClient configures and returns a new API client for the Edge service. +func ConfigureClient(p *print.Printer, cliVersion string) (APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.EdgeCustomEndpointKey), false, edge.NewAPIClient) +} diff --git a/internal/pkg/services/edge/common/error/error.go b/internal/pkg/services/edge/common/error/error.go new file mode 100755 index 000000000..2fd1433c3 --- /dev/null +++ b/internal/pkg/services/edge/common/error/error.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +// Package error provides custom error types for STACKIT Edge Cloud operations. +// +// This package defines structured error types that provide better error handling +// and type checking compared to simple string errors. Each error type can carry +// additional context and implements the standard error interface. +package error + +import ( + "fmt" +) + +// NoIdentifierError indicates that no identifier was provided when one was required. +type NoIdentifierError struct { + Operation string // Optional: which operation failed +} + +func (e *NoIdentifierError) Error() string { + if e.Operation != "" { + return fmt.Sprintf("no identifier provided for %s", e.Operation) + } + return "no identifier provided" +} + +// InvalidIdentifierError indicates that an unsupported identifier was provided. +type InvalidIdentifierError struct { + Identifier string // The invalid identifier that was provided +} + +func (e *InvalidIdentifierError) Error() string { + if e.Identifier != "" { + return fmt.Sprintf("unsupported identifier provided: %s", e.Identifier) + } + return "unsupported identifier provided" +} + +// InstanceExistsError indicates that a specific instance already exists. +type InstanceExistsError struct { + DisplayName string // Optional: the display name that was searched for +} + +func (e *InstanceExistsError) Error() string { + if e.DisplayName != "" { + return fmt.Sprintf("instance already exists: %s", e.DisplayName) + } + return "instance already exists" +} + +// NoInstanceError indicates that no instance was provided in a context where one was expected. +type NoInstanceError struct { + Context string // Optional: context where no instance was found (e.g., "in response", "in project") +} + +func (e *NoInstanceError) Error() string { + if e.Context != "" { + return fmt.Sprintf("no instance provided %s", e.Context) + } + return "no instance provided" +} + +// NewNoIdentifierError creates a new NoIdentifierError with optional context. +func NewNoIdentifierError(operation string) *NoIdentifierError { + return &NoIdentifierError{Operation: operation} +} + +// NewInvalidIdentifierError creates a new InvalidIdentifierError with the provided identifier. +func NewInvalidIdentifierError(identifier string) *InvalidIdentifierError { + return &InvalidIdentifierError{ + Identifier: identifier, + } +} + +// NewInstanceExistsError creates a new InstanceExistsError with optional instance details. +func NewInstanceExistsError(displayName string) *InstanceExistsError { + return &InstanceExistsError{ + DisplayName: displayName, + } +} + +// NewNoInstanceError creates a new NoInstanceError with optional context. +func NewNoInstanceError(context string) *NoInstanceError { + return &NoInstanceError{Context: context} +} diff --git a/internal/pkg/services/edge/common/error/error_test.go b/internal/pkg/services/edge/common/error/error_test.go new file mode 100755 index 000000000..1268d898a --- /dev/null +++ b/internal/pkg/services/edge/common/error/error_test.go @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +// Unit tests for package error +package error + +import ( + "testing" + + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +func TestNoIdentifierError(t *testing.T) { + type args struct { + operation string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + operation: "", + }, + want: "no identifier provided", + }, + { + name: "with operation", + args: args{ + operation: "create", + }, + want: "no identifier provided for create", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&NoIdentifierError{Operation: tt.args.operation}).Error() + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestInvalidIdentifierError(t *testing.T) { + type args struct { + id string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + id: "", + }, + want: "unsupported identifier provided", + }, + { + name: "with identifier", + args: args{ + id: "x-123", + }, + want: "unsupported identifier provided: x-123", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&InvalidIdentifierError{Identifier: tt.args.id}).Error() + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestInstanceExistsError(t *testing.T) { + type args struct { + name string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{name: ""}, + want: "instance already exists"}, + { + name: "with display name", + args: args{name: "my-inst"}, + want: "instance already exists: my-inst", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&InstanceExistsError{DisplayName: tt.args.name}).Error() + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestNoInstanceError(t *testing.T) { + type args struct { + ctx string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "empty", + args: args{ + ctx: "", + }, + want: "no instance provided", + }, + { + name: "with context", + args: args{ + ctx: "in project", + }, + want: "no instance provided in project", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := (&NoInstanceError{Context: tt.args.ctx}).Error() + testUtils.AssertValue(t, got, tt.want) + }) + } +} + +func TestConstructorsReturnExpected(t *testing.T) { + tests := []struct { + name string + got any + want any + }{ + { + name: "NoIdentifier operation", + got: NewNoIdentifierError("op").Operation, + want: "op", + }, + { + name: "InvalidIdentifier identifier", + got: NewInvalidIdentifierError("id").Identifier, + want: "id", + }, + { + name: "InstanceExists displayName", + got: NewInstanceExistsError("name").DisplayName, + want: "name", + }, + { + name: "NoInstance context", + got: NewNoInstanceError("ctx").Context, + want: "ctx", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wantErr, wantIsErr := tt.want.(error) + gotErr, gotIsErr := tt.got.(error) + if wantIsErr { + if !gotIsErr { + t.Fatalf("expected error but got %T", tt.got) + } + testUtils.AssertError(t, gotErr, wantErr) + return + } + + testUtils.AssertValue(t, tt.got, tt.want) + }) + } +} diff --git a/internal/pkg/services/edge/common/instance/instance.go b/internal/pkg/services/edge/common/instance/instance.go new file mode 100644 index 000000000..6dc35c672 --- /dev/null +++ b/internal/pkg/services/edge/common/instance/instance.go @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package instance + +import ( + "fmt" + "regexp" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + cliUtils "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +// Validation constants taken from OpenApi spec. +const ( + displayNameMinimumChars = 4 + displayNameMaximumChars = 8 + displayNameRegex = `^[a-z]([-a-z0-9]*[a-z0-9])?$` + descriptionMaxLength = 256 + instanceIdMaxLength = 16 + instanceIdMinLength = displayNameMinimumChars + 1 // Instance ID is generated by extending the display name. +) + +// User input flags for instance commands +const ( + DisplayNameFlag = "name" // > displayNameMinimumChars <= displayNameMaximumChars characters + regex displayNameRegex + DescriptionFlag = "description" // <= descriptionMaxLength characters + PlanIdFlag = "plan-id" // UUID + InstanceIdFlag = "id" // instance id (unique per project) +) + +// Flag usage texts +const ( + DisplayNameUsage = "The displayed name to distinguish multiple instances." + DescriptionUsage = "A user chosen description to distinguish multiple instances." + PlanIdUsage = "Service Plan configures the size of the Instance." + InstanceIdUsage = "The project-unique identifier of this instance." +) + +// Flag shorthands +const ( + DisplayNameShorthand = "n" + DescriptionShorthand = "d" + InstanceIdShorthand = "i" +) + +// OpenApi generated code will have different types for by-instance-id and by-display-name API calls, which are currently impl. as separate endpoints. +// To make the code more flexible, we use a struct to hold the request model. +type RequestModel struct { + Value any +} + +func ValidateDisplayName(displayName *string) error { + if displayName == nil { + return &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s may not be empty", DisplayNameFlag), + } + } + + if len(*displayName) > displayNameMaximumChars { + return &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DisplayNameFlag, displayNameMaximumChars), + } + } + if len(*displayName) < displayNameMinimumChars { + return &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", DisplayNameFlag, displayNameMinimumChars), + } + } + displayNameRegex := regexp.MustCompile(displayNameRegex) + if !displayNameRegex.MatchString(*displayName) { + return &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), + } + } + return nil +} + +func ValidatePlanId(planId *string) error { + if planId == nil { + return &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s may not be empty", PlanIdFlag), + } + } + + err := cliUtils.ValidateUUID(*planId) + if err != nil { + return &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s is not a valid UUID: %v", PlanIdFlag, err), + } + } + return nil +} + +func ValidateDescription(description string) error { + if len(description) > descriptionMaxLength { + return &cliErr.FlagValidationError{ + Flag: DescriptionFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DescriptionFlag, descriptionMaxLength), + } + } + + return nil +} + +func ValidateInstanceId(instanceId *string) error { + if instanceId == nil { + return &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), + } + } + + if *instanceId == "" { + return &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), + } + } + if len(*instanceId) < instanceIdMinLength { + return &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", InstanceIdFlag, instanceIdMinLength), + } + } + if len(*instanceId) > instanceIdMaxLength { + return &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", InstanceIdFlag, instanceIdMaxLength), + } + } + + return nil +} diff --git a/internal/pkg/services/edge/common/instance/instance_test.go b/internal/pkg/services/edge/common/instance/instance_test.go new file mode 100755 index 000000000..70a7dd11d --- /dev/null +++ b/internal/pkg/services/edge/common/instance/instance_test.go @@ -0,0 +1,348 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package instance + +import ( + "fmt" + "strings" + "testing" + + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +func TestValidateDisplayName(t *testing.T) { + type args struct { + displayName *string + } + tests := []struct { + name string + args *args + want error + }{ + // Valid cases + { + name: "valid minimum length", + args: &args{displayName: utils.Ptr("test")}, + }, + { + name: "valid maximum length", + args: &args{displayName: utils.Ptr("testname")}, + }, + { + name: "valid with hyphens", + args: &args{displayName: utils.Ptr("test-app")}, + }, + { + name: "valid with numbers", + args: &args{displayName: utils.Ptr("test123")}, + }, + { + name: "valid starting with letter", + args: &args{displayName: utils.Ptr("a-test")}, + }, + + // Error cases - nil pointer + { + name: "nil display name", + args: &args{displayName: nil}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s may not be empty", DisplayNameFlag), + }, + }, + + // Error cases - length validation + { + name: "too short", + args: &args{displayName: utils.Ptr("abc")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", DisplayNameFlag, displayNameMinimumChars), + }, + }, + { + name: "too long", + args: &args{displayName: utils.Ptr("verylongname")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DisplayNameFlag, displayNameMaximumChars), + }, + }, + + // Error cases - regex validation + { + name: "starts with number", + args: &args{displayName: utils.Ptr("1test")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), + }, + }, + { + name: "starts with hyphen", + args: &args{displayName: utils.Ptr("-test")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), + }, + }, + { + name: "ends with hyphen", + args: &args{displayName: utils.Ptr("test-")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), + }, + }, + { + name: "contains uppercase", + args: &args{displayName: utils.Ptr("Test")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), + }, + }, + { + name: "contains special characters", + args: &args{displayName: utils.Ptr("test@")}, + want: &cliErr.FlagValidationError{ + Flag: DisplayNameFlag, + Details: fmt.Sprintf("%s didn't match the required regex expression %s", DisplayNameFlag, displayNameRegex), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateDisplayName(tt.args.displayName) + testUtils.AssertError(t, err, tt.want) + }) + } +} + +func TestValidatePlanId(t *testing.T) { + type args struct { + planId *string + } + tests := []struct { + name string + args *args + want error + }{ + // Valid cases + { + name: "valid UUID v4", + args: &args{planId: utils.Ptr("550e8400-e29b-41d4-a716-446655440000")}, + }, + { + name: "valid UUID lowercase", + args: &args{planId: utils.Ptr("6ba7b810-9dad-11d1-80b4-00c04fd430c8")}, + }, + { + name: "valid UUID uppercase", + args: &args{planId: utils.Ptr("6BA7B810-9DAD-11D1-80B4-00C04FD430C8")}, + }, + { + name: "valid UUID without hyphens", + args: &args{planId: utils.Ptr("550e8400e29b41d4a716446655440000")}, + }, + + // Error cases - nil pointer + { + name: "nil plan id", + args: &args{planId: nil}, + want: &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s may not be empty", PlanIdFlag), + }, + }, + + // Error cases - invalid UUID format + { + name: "invalid UUID - too short", + args: &args{planId: utils.Ptr("550e8400-e29b-41d4-a716")}, + want: &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s is not a valid UUID: parse 550e8400-e29b-41d4-a716 as UUID: invalid UUID length: 23", PlanIdFlag), + }, + }, + { + name: "invalid UUID - invalid characters", + args: &args{planId: utils.Ptr("550e8400-e29b-41d4-a716-44665544000g")}, + want: &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s is not a valid UUID: parse 550e8400-e29b-41d4-a716-44665544000g as UUID: invalid UUID format", PlanIdFlag), + }, + }, + { + name: "not a UUID", + args: &args{planId: utils.Ptr("not-a-uuid")}, + want: &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s is not a valid UUID: parse not-a-uuid as UUID: invalid UUID length: 10", PlanIdFlag), + }, + }, + { + name: "empty string", + args: &args{planId: utils.Ptr("")}, + want: &cliErr.FlagValidationError{ + Flag: PlanIdFlag, + Details: fmt.Sprintf("%s is not a valid UUID: parse as UUID: invalid UUID length: 0", PlanIdFlag), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidatePlanId(tt.args.planId) + testUtils.AssertError(t, err, tt.want) + }) + } +} + +func TestValidateDescription(t *testing.T) { + type args struct { + description string + } + tests := []struct { + name string + args *args + want error + }{ + // Valid cases + { + name: "empty description", + args: &args{description: ""}, + }, + { + name: "short description", + args: &args{description: "A short description"}, + }, + { + name: "description at maximum length", + args: &args{description: strings.Repeat("a", descriptionMaxLength)}, + }, + { + name: "description with special characters", + args: &args{description: "Description with special chars: !@#$%^&*()"}, + }, + { + name: "description with unicode", + args: &args{description: "Description with unicode: 你好世界 🌍"}, + }, + + // Error cases + { + name: "description too long", + args: &args{description: strings.Repeat("a", descriptionMaxLength+1)}, + want: &cliErr.FlagValidationError{ + Flag: DescriptionFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DescriptionFlag, descriptionMaxLength), + }, + }, + { + name: "description way too long", + args: &args{description: strings.Repeat("a", descriptionMaxLength+100)}, + want: &cliErr.FlagValidationError{ + Flag: DescriptionFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", DescriptionFlag, descriptionMaxLength), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateDescription(tt.args.description) + testUtils.AssertError(t, err, tt.want) + }) + } +} + +func TestValidateInstanceId(t *testing.T) { + type args struct { + instanceId *string + } + tests := []struct { + name string + args *args + want error + }{ + // Valid cases + { + name: "valid instance id at minimum length", + args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMinLength))}, + }, + { + name: "valid instance id at maximum length", + args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMaxLength))}, + }, + { + name: "valid instance id with mixed characters", + args: &args{instanceId: utils.Ptr("test-instance")}, + }, + + // Error cases - nil pointer + { + name: "nil instance id", + args: &args{instanceId: nil}, + want: &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), + }, + }, + + // Error cases - empty string + { + name: "empty string", + args: &args{instanceId: utils.Ptr("")}, + want: &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s may not be empty", InstanceIdFlag), + }, + }, + + // Error cases - length validation + { + name: "too short", + args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMinLength-1))}, + want: &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", InstanceIdFlag, instanceIdMinLength), + }, + }, + { + name: "way too short", + args: &args{instanceId: utils.Ptr("a")}, + want: &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s is too short (minimum length is %d characters)", InstanceIdFlag, instanceIdMinLength), + }, + }, + { + name: "too long", + args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMaxLength+1))}, + want: &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", InstanceIdFlag, instanceIdMaxLength), + }, + }, + { + name: "way too long", + args: &args{instanceId: utils.Ptr(strings.Repeat("a", instanceIdMaxLength+10))}, + want: &cliErr.FlagValidationError{ + Flag: InstanceIdFlag, + Details: fmt.Sprintf("%s is too long (maximum length is %d characters)", InstanceIdFlag, instanceIdMaxLength), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateInstanceId(tt.args.instanceId) + testUtils.AssertError(t, err, tt.want) + }) + } +} diff --git a/internal/pkg/services/edge/common/kubeconfig/kubeconfig.go b/internal/pkg/services/edge/common/kubeconfig/kubeconfig.go new file mode 100755 index 000000000..ef0918c8b --- /dev/null +++ b/internal/pkg/services/edge/common/kubeconfig/kubeconfig.go @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package kubeconfig + +import ( + "fmt" + "maps" + "math" + "os" + "path/filepath" + + "k8s.io/client-go/tools/clientcmd" +) + +// Validation constants taken from OpenApi spec. +const ( + expirationSecondsMax = 15552000 // 60 * 60 * 24 * 180 seconds = 180 days + expirationSecondsMin = 600 // 60 * 10 seconds = 10 minutes +) + +// Defaults taken from OpenApi spec. +const ( + ExpirationSecondsDefault = 3600 // 60 * 60 seconds = 1 hour +) + +// User input flags for kubeconfig commands +const ( + ExpirationFlag = "expiration" + DisableWritingFlag = "disable-writing" + FilepathFlag = "filepath" + OverwriteFlag = "overwrite" + SwitchContextFlag = "switch-context" +) + +// Flag usage texts +const ( + ExpirationUsage = "Expiration time for the kubeconfig, e.g. 5d. By default, the token is valid for 1h." + FilepathUsage = "Path to the kubeconfig file. A default is chosen by Kubernetes if not set." + DisableWritingUsage = "Disable writing the kubeconfig to a file." + OverwriteUsage = "Force overwrite the kubeconfig file if it exists." + SwitchContextUsage = "Switch to the context in the kubeconfig file to the new context." +) + +// Flag shorthands +const ( + ExpirationShorthand = "e" + DisableWritingShorthand = "" + FilepathShorthand = "f" + OverwriteShorthand = "" + SwitchContextShorthand = "" +) + +func ValidateExpiration(expiration *uint64) error { + if expiration != nil { + // We're using utils.ConvertToSeconds to convert the user input string to seconds, which is using + // math.MaxUint64 internally, if no special limits are set. However: the OpenApi v3 Spec + // only allows integers (int64). So we could end up in a overflow IF expirationSecondsMax + // ever is changed beyond the maximum value of int64. This check makes sure this won't happen. + maxExpiration := uint64(math.Min(expirationSecondsMax, math.MaxInt64)) + if *expiration > maxExpiration { + return fmt.Errorf("%s is too large (maximum is %d seconds)", ExpirationFlag, maxExpiration) + } + // If expiration is ever changed to int64 this check makes sure we never end up with negative expiration times. + minExpiration := uint64(math.Max(expirationSecondsMin, 0)) + if *expiration < minExpiration { + return fmt.Errorf("%s is too small (minimum is %d seconds)", ExpirationFlag, minExpiration) + } + } + return nil +} + +// EmptyKubeconfigError is returned when the kubeconfig content is empty. +type EmptyKubeconfigError struct{} + +// Error returns the error message. +func (e *EmptyKubeconfigError) Error() string { + return "no data for kubeconfig" +} + +// LoadKubeconfigError is returned when loading the kubeconfig fails. +type LoadKubeconfigError struct { + Err error +} + +// Error returns the error message. +func (e *LoadKubeconfigError) Error() string { + return fmt.Sprintf("load kubeconfig: %v", e.Err) +} + +// Unwrap returns the underlying error. +func (e *LoadKubeconfigError) Unwrap() error { + return e.Err +} + +// WriteKubeconfigError is returned when writing the kubeconfig fails. +type WriteKubeconfigError struct { + Err error +} + +// Error returns the error message. +func (e *WriteKubeconfigError) Error() string { + return fmt.Sprintf("write kubeconfig: %v", e.Err) +} + +// Unwrap returns the underlying error. +func (e *WriteKubeconfigError) Unwrap() error { + return e.Err +} + +// InvalidKubeconfigPathError is returned when an invalid kubeconfig path is provided. +type InvalidKubeconfigPathError struct { + Path string +} + +// Error returns the error message. +func (e *InvalidKubeconfigPathError) Error() string { + return fmt.Sprintf("invalid path: %s", e.Path) +} + +// mergeKubeconfig merges new kubeconfig data into a kubeconfig file. +// +// If the destination file does not exist, it will be created. If the file exists, +// the new data (clusters, contexts, and users) is merged into the existing +// configuration, overwriting entries with the same name and replacing the +// current-context if defined in the new data. +// +// The function takes the following parameters: +// - configPath: The path to the destination file. The file and the directory tree +// for the file will be created if it does not exist. +// - data: The new kubeconfig content to merge. Merge is performed based on standard +// kubeconfig structure. +// - switchContext: If true, the function will switch to the new context in the +// kubeconfig file after merging. +// +// It returns a nil error on success. On failure, it returns an error indicating +// if the provided data was empty, malformed, or if there were issues reading from +// or writing to the filesystem. +func mergeKubeconfig(filePath *string, data string, switchContext bool) error { + if filePath == nil { + return fmt.Errorf("no kubeconfig file provided to be merged") + } + path := *filePath + + // Check if the new kubeconfig data is empty + if data == "" { + return &EmptyKubeconfigError{} + } + + // Load and validate the data into a kubeconfig object + newConfig, err := clientcmd.Load([]byte(data)) + if err != nil { + return &LoadKubeconfigError{Err: err} + } + + // If the destination kubeconfig does not exist, create a new one. IsNotExist will ignore other errors. + // Other errors are handled separately by the following clientcmd.LoadFromFile clientcmd.LoadFromFile + if _, err := os.Stat(path); os.IsNotExist(err) { + return writeKubeconfig(&path, data) + } + + // If the file exists load and validate the existing kubeconfig into a config object + existingConfig, err := clientcmd.LoadFromFile(path) + if err != nil { + return &LoadKubeconfigError{Err: err} + } + + // Merge the new kubeconfig data into the existing config object + maps.Copy(existingConfig.AuthInfos, newConfig.AuthInfos) + maps.Copy(existingConfig.Clusters, newConfig.Clusters) + maps.Copy(existingConfig.Contexts, newConfig.Contexts) + + // If no CurrentContext is set or switchContext is true, set the CurrentContext to the CurrentContext of the new kubeconfig + if newConfig.CurrentContext != "" && (switchContext || existingConfig.CurrentContext == "") { + existingConfig.CurrentContext = newConfig.CurrentContext + } + + // Save the merged config to the file, creating missing directories as needed. + if err := clientcmd.WriteToFile(*existingConfig, path); err != nil { + return &WriteKubeconfigError{Err: err} + } + + return nil +} + +// writeKubeconfig writes kubeconfig data to a file, overwriting it if it exists. +// +// The function takes the following parameters: +// - configPath: The path to the destination file. The file and the directory tree +// for the file will be created if it does not exist. +// - data: The new kubeconfig content to write to the file. +// +// It returns a nil error on success. On failure, it returns an error indicating +// if the provided data was empty, malformed, or if there were issues reading from +// or writing to the filesystem. +func writeKubeconfig(filePath *string, data string) error { + if filePath == nil { + return fmt.Errorf("no kubeconfig file provided to be written") + } + path := *filePath + + // Check if the new kubeconfig data is empty + if data == "" { + return &EmptyKubeconfigError{} + } + + // Load and validate the data into a kubeconfig object + config, err := clientcmd.Load([]byte(data)) + if err != nil { + return &LoadKubeconfigError{Err: err} + } + + // Save the merged config to the file, creating missing directories as needed. + if err := clientcmd.WriteToFile(*config, path); err != nil { + return &WriteKubeconfigError{Err: err} + } + + return nil +} + +// getDefaultKubeconfigPath returns the default location for the kubeconfig file, +// following standard Kubernetes loading rules. +// +// It returns a string containing the absolute path to the default kubeconfig file. +func getDefaultKubeconfigPath() string { + return clientcmd.NewDefaultClientConfigLoadingRules().GetDefaultFilename() +} + +// Returns the absolute path to the kubeconfig file. +// If a file path is provided, it is validated and, if valid, returned as an absolute path. +// If nil is provided the default kubeconfig path is loaded and returned as an absolute path. +func getKubeconfigPath(filePath *string) (string, error) { + if filePath == nil { + return getDefaultKubeconfigPath(), nil + } + + if isValidFilePath(filePath) { + return filepath.Abs(*filePath) + } + return "", &InvalidKubeconfigPathError{Path: *filePath} +} + +// Basic filesystem path validation. Returns true if the provided string is a path. Returns false otherwise. +func isValidFilePath(filePath *string) bool { + if filePath == nil || *filePath == "" { + return false + } + + // Clean the path and check if it's valid + cleaned := filepath.Clean(*filePath) + if cleaned == "." || cleaned == string(filepath.Separator) { + return false + } + + // Try to get absolute path (this will fail for invalid paths) + _, err := filepath.Abs(*filePath) + // If no error, the path is valid (return true). Otherwise, it's invalid (return false). + return err == nil +} + +// Basic filesystem file existence check. Returns true if the file exists. Returns false otherwise. +func isExistingFile(filePath *string) bool { + // Check if the kubeconfig file exists + _, errStat := os.Stat(*filePath) + return !os.IsNotExist(errStat) +} + +// ConfirmationCallback is a function that prompts for confirmation with the given message +// and returns true if confirmed, false otherwise +type ConfirmationCallback func(message string) error + +// WriteOptions contains options for writing kubeconfig files +type WriteOptions struct { + Overwrite bool + SwitchContext bool + ConfirmFn ConfirmationCallback +} + +// WithOverwrite sets whether to overwrite existing files instead of merging +func (w WriteOptions) WithOverwrite(overwrite bool) WriteOptions { + w.Overwrite = overwrite + return w +} + +// WithSwitchContext sets whether to switch to the new context after writing +func (w WriteOptions) WithSwitchContext(switchContext bool) WriteOptions { + w.SwitchContext = switchContext + return w +} + +// WithConfirmation sets the confirmation callback function +func (w WriteOptions) WithConfirmation(fn ConfirmationCallback) WriteOptions { + w.ConfirmFn = fn + return w +} + +// NewWriteOptions creates a new WriteOptions with default values +func NewWriteOptions() WriteOptions { + return WriteOptions{ + Overwrite: false, + SwitchContext: false, + ConfirmFn: nil, + } +} + +// WriteKubeconfig writes the provided kubeconfig data to a file on the filesystem. +// By default, if the file already exists, it will be merged with the provided data. +// This behavior can be controlled using the provided options. +// +// The function takes the following parameters: +// - filePath: The path to the destination file. The file and the directory tree for the +// file will be created if it does not exist. If nil, the default kubeconfig path is used. +// - kubeconfig: The kubeconfig content to write. +// - options: Options for controlling the write behavior. +// +// It returns the file path actually used to write to on success. +func WriteKubeconfig(filePath *string, kubeconfig string, options WriteOptions) (*string, error) { + // Check if the provided filePath is valid or use the default kubeconfig path no filePath is provided + path, err := getKubeconfigPath(filePath) + if err != nil { + return nil, err + } + + if isExistingFile(&path) { + // If the file exists + if !options.Overwrite { + // If overwrite was not requested the default it to merge + if options.ConfirmFn != nil { + // If confirmation callback is provided, prompt the user for confirmation + prompt := fmt.Sprintf("Update your kubeconfig %q?", path) + err := options.ConfirmFn(prompt) + if err != nil { + // If the user doesn't confirm do not proceed with the merge + return nil, err + } + } + err := mergeKubeconfig(&path, kubeconfig, options.SwitchContext) + if err != nil { + return nil, err + } + return &path, err + } + // If overwrite was requested overwrite the existing file + if options.ConfirmFn != nil { + // If confirmation callback is provided, prompt the user for confirmation + prompt := fmt.Sprintf("Replace your kubeconfig %q?", path) + err := options.ConfirmFn(prompt) + if err != nil { + // If the user doesn't confirm do not proceed with the overwrite + return nil, err + } + // Fallthrough + } + } + // If the file doesn't exist or in case the user confirmed the overwrite (fallthrough) write the file + err = writeKubeconfig(&path, kubeconfig) + if err != nil { + return nil, err + } + return &path, err +} diff --git a/internal/pkg/services/edge/common/kubeconfig/kubeconfig_test.go b/internal/pkg/services/edge/common/kubeconfig/kubeconfig_test.go new file mode 100755 index 000000000..e196052c4 --- /dev/null +++ b/internal/pkg/services/edge/common/kubeconfig/kubeconfig_test.go @@ -0,0 +1,745 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package kubeconfig + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "k8s.io/client-go/tools/clientcmd" + + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var ( + testErrorMessage = "test error message" + errStringErrTest = errors.New(testErrorMessage) +) + +const ( + kubeconfig_1_yaml = ` +apiVersion: v1 +clusters: +- cluster: + server: https://server-1.com + name: cluster-1 +contexts: +- context: + cluster: cluster-1 + user: user-1 + name: context-1 +current-context: context-1 +kind: Config +preferences: {} +users: +- name: user-1 + user: {} +` + kubeconfig_2_yaml = ` +apiVersion: v1 +clusters: +- cluster: + server: https://server-2.com + name: cluster-2 +contexts: +- context: + cluster: cluster-2 + user: user-2 + name: context-2 +current-context: context-2 +kind: Config +users: +- name: user-2 + user: {} +` + overwriteKubeconfigTarget = ` +apiVersion: v1 +clusters: +- cluster: + server: https://server-1.com + name: cluster-1 +contexts: +- context: + cluster: cluster-1 + user: user-1 + name: context-1 +current-context: context-1 +kind: Config +users: +- name: user-1 + user: + token: old-token +` + overwriteKubeconfigSource = ` +apiVersion: v1 +clusters: +- cluster: + server: https://server-1-new.com + name: cluster-1 +contexts: +- context: + cluster: cluster-1 + user: user-1 + name: context-1 +current-context: context-1 +kind: Config +users: +- name: user-1 + user: + token: new-token +` +) + +func TestValidateExpiration(t *testing.T) { + type args struct { + expiration *uint64 + } + tests := []struct { + name string + args *args + want error + }{ + // Valid cases + { + name: "nil expiration", + args: &args{ + expiration: nil, + }, + }, + { + name: "valid expiration - minimum value", + args: &args{ + expiration: utils.Ptr(uint64(expirationSecondsMin)), + }, + }, + { + name: "valid expiration - maximum value", + args: &args{ + expiration: utils.Ptr(uint64(expirationSecondsMax)), + }, + }, + { + name: "valid expiration - default value", + args: &args{ + expiration: utils.Ptr(uint64(ExpirationSecondsDefault)), + }, + }, + { + name: "valid expiration - middle value", + args: &args{ + expiration: utils.Ptr(uint64(86400)), // 1 day + }, + }, + + // Error cases - below minimum + { + name: "expiration too small - below minimum", + args: &args{ + expiration: utils.Ptr(uint64(expirationSecondsMin - 1)), + }, + want: fmt.Errorf("%s is too small (minimum is %d seconds)", ExpirationFlag, expirationSecondsMin), + }, + { + name: "expiration too small - zero", + args: &args{ + expiration: utils.Ptr(uint64(0)), + }, + want: fmt.Errorf("%s is too small (minimum is %d seconds)", ExpirationFlag, expirationSecondsMin), + }, + + // Error cases - above maximum + { + name: "expiration too large - above maximum", + args: &args{ + expiration: utils.Ptr(uint64(expirationSecondsMax + 1)), + }, + want: fmt.Errorf("%s is too large (maximum is %d seconds)", ExpirationFlag, expirationSecondsMax), + }, + { + name: "expiration too large - way above maximum", + args: &args{ + expiration: utils.Ptr(uint64(9999999999999999999)), + }, + want: fmt.Errorf("%s is too large (maximum is %d seconds)", ExpirationFlag, expirationSecondsMax), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateExpiration(tt.args.expiration) + testUtils.AssertError(t, err, tt.want) + }) + } +} + +func TestErrors(t *testing.T) { + type args struct { + err error + } + tests := []struct { + name string + args *args + wantErr error + }{ + // EmptyKubeconfigError + { + name: "EmptyKubeconfigError", + args: &args{ + err: &EmptyKubeconfigError{}, + }, + wantErr: &EmptyKubeconfigError{}, + }, + + // LoadKubeconfigError + { + name: "LoadKubeconfigError", + args: &args{ + err: &LoadKubeconfigError{Err: errStringErrTest}, + }, + wantErr: errStringErrTest, + }, + + // WriteKubeconfigError + { + name: "WriteKubeconfigError", + args: &args{ + err: &WriteKubeconfigError{Err: errStringErrTest}, + }, + wantErr: errStringErrTest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testUtils.AssertError(t, tt.args.err, tt.wantErr) + }) + } +} + +// Already have comprehensive tests for WriteKubeconfig + +func TestWriteOptions(t *testing.T) { + confirmFn := func(_ string) error { return nil } + + type args struct { + modify func(WriteOptions) WriteOptions + check func(*testing.T, WriteOptions) + } + tests := []struct { + name string + args *args + }{ + // Default options + { + name: "NewWriteOptions creates default options", + args: &args{ + modify: func(o WriteOptions) WriteOptions { return o }, + check: func(t *testing.T, opts WriteOptions) { + if opts.Overwrite { + t.Error("expected Overwrite to be false by default") + } + if opts.SwitchContext { + t.Error("expected SwitchContext to be false by default") + } + if opts.ConfirmFn != nil { + t.Error("expected ConfirmFn to be nil by default") + } + }, + }, + }, + + // Individual option tests + { + name: "WithOverwrite sets overwrite flag", + args: &args{ + modify: func(o WriteOptions) WriteOptions { return o.WithOverwrite(true) }, + check: func(t *testing.T, opts WriteOptions) { + if !opts.Overwrite { + t.Error("expected Overwrite to be true") + } + }, + }, + }, + { + name: "WithSwitchContext sets switch context flag", + args: &args{ + modify: func(o WriteOptions) WriteOptions { return o.WithSwitchContext(true) }, + check: func(t *testing.T, opts WriteOptions) { + if !opts.SwitchContext { + t.Error("expected SwitchContext to be true") + } + }, + }, + }, + { + name: "WithConfirmation sets confirmation callback", + args: &args{ + modify: func(o WriteOptions) WriteOptions { return o.WithConfirmation(confirmFn) }, + check: func(t *testing.T, opts WriteOptions) { + if opts.ConfirmFn == nil { + t.Error("expected ConfirmFn to be set") + } + }, + }, + }, + + // Chained options + { + name: "options are chainable", + args: &args{ + modify: func(o WriteOptions) WriteOptions { + return o.WithOverwrite(true). + WithSwitchContext(true). + WithConfirmation(confirmFn) + }, + check: func(t *testing.T, opts WriteOptions) { + if !opts.Overwrite { + t.Error("expected Overwrite to be true") + } + if !opts.SwitchContext { + t.Error("expected SwitchContext to be true") + } + if opts.ConfirmFn == nil { + t.Error("expected ConfirmFn to be set") + } + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := tt.args.modify(NewWriteOptions()) + tt.args.check(t, opts) + }) + } +} + +func TestGetDefaultKubeconfigPath(t *testing.T) { + type args struct { + kubeconfigEnv *string // nil means unset + } + tests := []struct { + name string + args *args + want string + }{ + // KUBECONFIG not set + { + name: "returns a non-empty path when KUBECONFIG is not set", + args: &args{kubeconfigEnv: nil}, + want: "", + }, + + // Single path + { + name: "returns path from KUBECONFIG if set", + args: &args{kubeconfigEnv: utils.Ptr("/test/kubeconfig_1_yaml")}, + want: "/test/kubeconfig_1_yaml", + }, + + // Multiple paths + { + name: "returns first path from KUBECONFIG if multiple are set", + args: &args{kubeconfigEnv: utils.Ptr("/test/kubeconfig_1_yaml" + string(os.PathListSeparator) + "/test/kubeconfig_2_yaml")}, + want: "/test/kubeconfig_1_yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save original env and restore after test + oldKubeconfig := os.Getenv("KUBECONFIG") + defer func() { + if err := os.Setenv("KUBECONFIG", oldKubeconfig); err != nil { + t.Logf("failed to restore KUBECONFIG: %v", err) + } + }() + + // Setup test environment + if tt.args.kubeconfigEnv == nil { + if err := os.Unsetenv("KUBECONFIG"); err != nil { + t.Fatalf("failed to unset KUBECONFIG: %v", err) + } + } else { + if err := os.Setenv("KUBECONFIG", *tt.args.kubeconfigEnv); err != nil { + t.Fatalf("failed to set KUBECONFIG: %v", err) + } + } + + // Run test + got := getDefaultKubeconfigPath() + + // If want is empty only make sure the returned path is not empty + // In that case we don't care about what path is default, only that one is. + want := filepath.Clean(tt.want) + if want == filepath.Clean("") { + if filepath.Clean(got) != "" { + return + } + } + + // Verify results + testUtils.AssertValue(t, filepath.Clean(got), want) + }) + } +} + +func TestGetKubeconfigPath(t *testing.T) { + type args struct { + path *string + checkPath func(t *testing.T, path string) + } + tests := []struct { + name string + args *args + wantErr error + }{ + { + name: "uses default path when nil provided", + args: &args{ + path: nil, + checkPath: func(t *testing.T, path string) { + if path == "" { + t.Error("expected non-empty path") + } + }, + }, + }, + { + name: "validates and returns absolute path when valid path provided", + args: &args{ + path: utils.Ptr("/tmp/kubeconfig"), + checkPath: func(t *testing.T, path string) { + if !filepath.IsAbs(path) { + t.Error("expected absolute path") + } + }, + }, + }, + { + name: "returns error for invalid path", + args: &args{ + path: utils.Ptr("."), + }, + wantErr: &InvalidKubeconfigPathError{Path: "."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path, err := getKubeconfigPath(tt.args.path) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + if tt.args.checkPath != nil { + tt.args.checkPath(t, path) + } + }) + } +} + +func TestIsValidFilePath(t *testing.T) { + type args struct { + path *string + } + tests := []struct { + name string + args *args + + want bool + }{ + { + name: "valid path", + args: &args{ + path: utils.Ptr("/test/kubeconfig"), + }, + want: true, + }, + { + name: "nil path", + args: &args{ + path: nil, + }, + want: false, + }, + { + name: "empty path", + args: &args{ + path: utils.Ptr(""), + }, + want: false, + }, + { + name: "single dot", + args: &args{ + path: utils.Ptr("."), + }, + want: false, + }, + { + name: "single slash", + args: &args{ + path: utils.Ptr("/"), + }, + want: false, + }, + { + name: "relative path with parent directory", + args: &args{ + path: utils.Ptr("../kubeconfig"), + }, + want: true, + }, + { + name: "path with spaces", + args: &args{ + path: utils.Ptr("/test/kube config"), + }, + want: true, + }, + { + name: "complex but valid path", + args: &args{ + path: utils.Ptr("/test/kube-config.d/cluster1/config"), + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isValidFilePath(tt.args.path); got != tt.want { + t.Errorf("isValidFilePath() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestWriteKubeconfig(t *testing.T) { + testPath := filepath.Join(t.TempDir(), "config") + defaultTempFile := filepath.Join(t.TempDir(), "default-kubeconfig") + + type args struct { + path *string + content string + options WriteOptions + setupEnv func() + checkFile func(t *testing.T, path string) + } + tests := []struct { + name string + args *args + wantPath *string + wantErr any + }{ + { + name: "writes new file with default options", + args: &args{ + path: &testPath, + content: kubeconfig_1_yaml, + options: NewWriteOptions(), + checkFile: func(t *testing.T, path string) { + if !isExistingFile(&path) { + t.Error("file was not created") + } + }, + }, + wantPath: &testPath, + }, + { + name: "handles invalid file path", + args: &args{ + path: utils.Ptr("."), + content: kubeconfig_1_yaml, + options: NewWriteOptions(), + }, + wantErr: &InvalidKubeconfigPathError{Path: "."}, + }, + { + name: "handles empty kubeconfig", + args: &args{ + path: &testPath, + content: "", + options: NewWriteOptions(), + }, + wantErr: &EmptyKubeconfigError{}, + }, + { + name: "uses default path when nil provided", + args: &args{ + path: nil, + content: kubeconfig_1_yaml, + options: NewWriteOptions(), + setupEnv: func() { + t.Setenv("KUBECONFIG", defaultTempFile) + }, + }, + wantPath: &defaultTempFile, + }, + { + name: "overwrites existing file when option is set", + args: &args{ + path: &testPath, + content: kubeconfig_2_yaml, + options: NewWriteOptions().WithOverwrite(true), + setupEnv: func() { + // Pre-write first file + if _, err := WriteKubeconfig(&testPath, kubeconfig_1_yaml, NewWriteOptions()); err != nil { + t.Fatalf("failed to setup test: %v", err) + } + }, + checkFile: func(t *testing.T, path string) { + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed to read kubeconfig: %v", err) + } + if !strings.Contains(string(content), "server-2.com") { + t.Error("file was not overwritten") + } + }, + }, + wantPath: &testPath, + }, + { + name: "respects user confirmation - confirmed", + args: &args{ + path: &testPath, + content: kubeconfig_1_yaml, + options: NewWriteOptions().WithConfirmation(func(_ string) error { + return nil + }), + }, + wantPath: &testPath, + }, + { + name: "respects user confirmation - denied", + args: &args{ + path: &testPath, + content: kubeconfig_1_yaml, + options: NewWriteOptions().WithConfirmation(func(_ string) error { + return errStringErrTest + }), + }, + wantErr: errStringErrTest, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.setupEnv != nil { + tt.args.setupEnv() + } + + got, gotErr := WriteKubeconfig(tt.args.path, tt.args.content, tt.args.options) + if !testUtils.AssertError(t, gotErr, tt.wantErr) { + return + } + + testUtils.AssertValue(t, got, tt.wantPath) + + if tt.args.checkFile != nil { + tt.args.checkFile(t, *got) + } + }) + } +} + +func TestMergeKubeconfig(t *testing.T) { + type args struct { + path *string + content string + switchCtx bool + setupEnv func() + } + tests := []struct { + name string + args args + verify func(t *testing.T, path string) + wantErr error + }{ + { + name: "merges configs with conflicting names", + args: args{ + path: utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), + content: overwriteKubeconfigSource, + switchCtx: true, + setupEnv: func() { + // Pre-write first file + if _, err := WriteKubeconfig(utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), overwriteKubeconfigTarget, NewWriteOptions()); err != nil { + t.Fatalf("failed to setup test: %v", err) + } + }, + }, + verify: func(t *testing.T, path string) { + config, err := clientcmd.LoadFromFile(path) + if err != nil { + t.Fatalf("failed to load merged config: %v", err) + } + + cluster := config.Clusters["cluster-1"] + if cluster.Server != "https://server-1-new.com" { + t.Errorf("expected server to be 'https://server-1-new.com', got '%s'", cluster.Server) + } + + user := config.AuthInfos["user-1"] + if user.Token != "new-token" { + t.Errorf("expected token to be 'new-token', got '%s'", user.Token) + } + }, + }, + { + name: "handles nil file path", + args: args{ + path: nil, + content: kubeconfig_1_yaml, + switchCtx: false, + }, + wantErr: fmt.Errorf("no kubeconfig file provided to be merged"), + }, + { + name: "handles invalid config", + args: args{ + path: utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), + content: "invalid yaml", + switchCtx: false, + }, + wantErr: &LoadKubeconfigError{}, + }, + { + name: "handles empty config", + args: args{ + path: utils.Ptr(filepath.Join(t.TempDir(), "kubeconfig")), + content: "", + switchCtx: false, + }, + wantErr: &EmptyKubeconfigError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.setupEnv != nil { + tt.args.setupEnv() + } + + err := mergeKubeconfig(tt.args.path, tt.args.content, tt.args.switchCtx) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + + if tt.verify != nil { + if tt.args.path == nil { + t.Fatalf("expected path to be set") + } + tt.verify(t, *tt.args.path) + } + }) + } +} diff --git a/internal/pkg/services/edge/common/validation/input.go b/internal/pkg/services/edge/common/validation/input.go new file mode 100644 index 000000000..c32f8a9be --- /dev/null +++ b/internal/pkg/services/edge/common/validation/input.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package validation + +import ( + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" +) + +// Struct to model the instance identifier provided by the user (either instance-id or display-name) +type Identifier struct { + Flag string + Value string +} + +// GetValidatedInstanceIdentifier gets and validates the instance identifier provided by the user through the command-line flags. +// It checks for either an instance ID or a display name and validates the provided value. +// +// p is the printer used for logging. +// cmd is the cobra command that holds the flags. +// +// Returns an Identifier struct containing the flag and its value if a valid identifier is provided, otherwise returns an error. +// Indirect unit tests of GetValidatedInstanceIdentifier are done within the respective CLI packages. +func GetValidatedInstanceIdentifier(p *print.Printer, cmd *cobra.Command) (*Identifier, error) { + switch { + case cmd.Flags().Changed(commonInstance.InstanceIdFlag): + instanceIdValue := flags.FlagToStringPointer(p, cmd, commonInstance.InstanceIdFlag) + if err := commonInstance.ValidateInstanceId(instanceIdValue); err != nil { + return nil, err + } + return &Identifier{ + Flag: commonInstance.InstanceIdFlag, + Value: *instanceIdValue, + }, nil + case cmd.Flags().Changed(commonInstance.DisplayNameFlag): + displayNameValue := flags.FlagToStringPointer(p, cmd, commonInstance.DisplayNameFlag) + if err := commonInstance.ValidateDisplayName(displayNameValue); err != nil { + return nil, err + } + return &Identifier{ + Flag: commonInstance.DisplayNameFlag, + Value: *displayNameValue, + }, nil + default: + return nil, commonErr.NewNoIdentifierError("") + } +} diff --git a/internal/pkg/services/edge/common/validation/input_test.go b/internal/pkg/services/edge/common/validation/input_test.go new file mode 100755 index 000000000..ed48999f1 --- /dev/null +++ b/internal/pkg/services/edge/common/validation/input_test.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package validation + +import ( + "testing" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + commonErr "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/error" + commonInstance "github.com/stackitcloud/stackit-cli/internal/pkg/services/edge/common/instance" + testUtils "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" +) + +func TestGetValidatedInstanceIdentifier(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + setup func(*cobra.Command) + want *Identifier + wantErr any + }{ + { + name: "instance id success", + setup: func(cmd *cobra.Command) { + cmd.Flags().String(commonInstance.InstanceIdFlag, "", "") + _ = cmd.Flags().Set(commonInstance.InstanceIdFlag, "edgesvc01") + }, + want: &Identifier{Flag: commonInstance.InstanceIdFlag, Value: "edgesvc01"}, + }, + { + name: "display name success", + setup: func(cmd *cobra.Command) { + cmd.Flags().String(commonInstance.DisplayNameFlag, "", "") + _ = cmd.Flags().Set(commonInstance.DisplayNameFlag, "edge01") + }, + want: &Identifier{Flag: commonInstance.DisplayNameFlag, Value: "edge01"}, + }, + { + name: "instance id validation error", + setup: func(cmd *cobra.Command) { + cmd.Flags().String(commonInstance.InstanceIdFlag, "", "") + _ = cmd.Flags().Set(commonInstance.InstanceIdFlag, "id") + }, + wantErr: "too short", + }, + { + name: "display name validation error", + setup: func(cmd *cobra.Command) { + cmd.Flags().String(commonInstance.DisplayNameFlag, "", "") + _ = cmd.Flags().Set(commonInstance.DisplayNameFlag, "x") + }, + wantErr: "too short", + }, + { + name: "no identifier", + setup: func(_ *cobra.Command) {}, + wantErr: &commonErr.NoIdentifierError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + params := testparams.NewTestParams() + cmd := &cobra.Command{Use: "test"} + tt.setup(cmd) + + got, err := GetValidatedInstanceIdentifier(params.Printer, cmd) + if !testUtils.AssertError(t, err, tt.wantErr) { + return + } + if tt.want != nil { + testUtils.AssertValue(t, got, tt.want) + } + }) + } +} diff --git a/internal/pkg/services/git/client/client.go b/internal/pkg/services/git/client/client.go index dde3b7be6..3b13e2e5c 100644 --- a/internal/pkg/services/git/client/client.go +++ b/internal/pkg/services/git/client/client.go @@ -1,45 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/git" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" ) -func ConfigureClient(p *print.Printer) (*git.APIClient, error) { - var err error - var apiClient *git.APIClient - var cfgOptions []sdkConfig.ConfigurationOption - - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions = append(cfgOptions, authCfgOption) - - customEndpoint := viper.GetString(config.GitCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err = git.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil +func ConfigureClient(p *print.Printer, cliVersion string) (*git.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.GitCustomEndpointKey), false, genericclient.CreateApiClient[*git.APIClient](git.NewAPIClient)) } diff --git a/internal/pkg/services/git/utils/utils.go b/internal/pkg/services/git/utils/utils.go index 3a875c920..77f7f119c 100644 --- a/internal/pkg/services/git/utils/utils.go +++ b/internal/pkg/services/git/utils/utils.go @@ -4,20 +4,13 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/git" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" ) -type GitClient interface { - GetInstanceExecute(ctx context.Context, projectId string, instanceId string) (*git.Instance, error) -} - -func GetInstanceName(ctx context.Context, apiClient GitClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) +func GetInstanceName(ctx context.Context, apiClient git.DefaultAPI, projectId, instanceId string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { return "", fmt.Errorf("get instance: %w", err) } - if resp.Name == nil { - return "", nil - } - return *resp.Name, nil + return resp.Name, nil } diff --git a/internal/pkg/services/git/utils/utils_test.go b/internal/pkg/services/git/utils/utils_test.go index 7ec5dc494..eb71c5e4e 100644 --- a/internal/pkg/services/git/utils/utils_test.go +++ b/internal/pkg/services/git/utils/utils_test.go @@ -5,20 +5,26 @@ import ( "fmt" "testing" + git "github.com/stackitcloud/stackit-sdk-go/services/git/v1betaapi" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/git" ) -type GitClientMocked struct { +type mockSettings struct { GetInstanceFails bool GetInstanceResp *git.Instance } -func (m *GitClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*git.Instance, error) { - if m.GetInstanceFails { - return nil, fmt.Errorf("could not get instance") +func newAPIMock(settings *mockSettings) git.DefaultAPI { + return &git.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ git.ApiGetInstanceRequest) (*git.Instance, error) { + if settings.GetInstanceFails { + return nil, fmt.Errorf("could not get instance details") + } + + return settings.GetInstanceResp, nil + }), } - return m.GetInstanceResp, nil } func TestGetinstanceName(t *testing.T) { @@ -30,10 +36,12 @@ func TestGetinstanceName(t *testing.T) { wantErr bool }{ { - name: "successful retrieval", - instanceResp: &git.Instance{Name: utils.Ptr("test-instance")}, - want: "test-instance", - wantErr: false, + name: "successful retrieval", + instanceResp: &git.Instance{ + Name: "test-instance", + }, + want: "test-instance", + wantErr: false, }, { name: "error on retrieval", @@ -49,10 +57,10 @@ func TestGetinstanceName(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - client := &GitClientMocked{ + client := newAPIMock(&mockSettings{ GetInstanceFails: tt.instanceErr, GetInstanceResp: tt.instanceResp, - } + }) got, err := GetInstanceName(context.Background(), client, "", "") if (err != nil) != tt.wantErr { t.Errorf("GetInstanceName() error = %v, wantErr %v", err, tt.wantErr) diff --git a/internal/pkg/services/iaas/client/client.go b/internal/pkg/services/iaas/client/client.go index e32d15b4c..16c0d84f6 100644 --- a/internal/pkg/services/iaas/client/client.go +++ b/internal/pkg/services/iaas/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*iaas.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.IaaSCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } else { - region := viper.GetString(config.RegionKey) - cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion(region)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := iaas.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.IaaSCustomEndpointKey), false, genericclient.CreateApiClient[*iaas.APIClient](iaas.NewAPIClient)) } diff --git a/internal/pkg/services/iaas/utils/utils.go b/internal/pkg/services/iaas/utils/utils.go index 2cf460334..43117701c 100644 --- a/internal/pkg/services/iaas/utils/utils.go +++ b/internal/pkg/services/iaas/utils/utils.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" ) var ( @@ -14,65 +14,47 @@ var ( ErrItemsNil = errors.New("items is nil") ) -type IaaSClient interface { - GetSecurityGroupRuleExecute(ctx context.Context, projectId, securityGroupRuleId, securityGroupId string) (*iaas.SecurityGroupRule, error) - GetSecurityGroupExecute(ctx context.Context, projectId, securityGroupId string) (*iaas.SecurityGroup, error) - GetPublicIPExecute(ctx context.Context, projectId, publicIpId string) (*iaas.PublicIp, error) - GetServerExecute(ctx context.Context, projectId, serverId string) (*iaas.Server, error) - GetVolumeExecute(ctx context.Context, projectId, volumeId string) (*iaas.Volume, error) - GetNetworkExecute(ctx context.Context, projectId, networkId string) (*iaas.Network, error) - GetNetworkAreaExecute(ctx context.Context, organizationId, areaId string) (*iaas.NetworkArea, error) - ListNetworkAreaProjectsExecute(ctx context.Context, organizationId, areaId string) (*iaas.ProjectListResponse, error) - GetNetworkAreaRangeExecute(ctx context.Context, organizationId, areaId, networkRangeId string) (*iaas.NetworkRange, error) - GetImageExecute(ctx context.Context, projectId string, imageId string) (*iaas.Image, error) - GetAffinityGroupExecute(ctx context.Context, projectId string, affinityGroupId string) (*iaas.AffinityGroup, error) - GetSnapshotExecute(ctx context.Context, projectId, snapshotId string) (*iaas.Snapshot, error) - GetBackupExecute(ctx context.Context, projectId, backupId string) (*iaas.Backup, error) -} - -func GetSecurityGroupRuleName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupRuleId, securityGroupId string) (string, error) { - resp, err := apiClient.GetSecurityGroupRuleExecute(ctx, projectId, securityGroupRuleId, securityGroupId) +func GetSecurityGroupRuleName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, securityGroupRuleId, securityGroupId string) (string, error) { + resp, err := apiClient.GetSecurityGroupRule(ctx, projectId, region, securityGroupRuleId, securityGroupId).Execute() if err != nil { return "", fmt.Errorf("get security group rule: %w", err) } - securityGroupRuleName := *resp.Ethertype + ", " + *resp.Direction + securityGroupRuleName := *resp.Ethertype + ", " + resp.Direction return securityGroupRuleName, nil } -func GetSecurityGroupName(ctx context.Context, apiClient IaaSClient, projectId, securityGroupId string) (string, error) { - resp, err := apiClient.GetSecurityGroupExecute(ctx, projectId, securityGroupId) +func GetSecurityGroupName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, securityGroupId string) (string, error) { + resp, err := apiClient.GetSecurityGroup(ctx, projectId, region, securityGroupId).Execute() if err != nil { return "", fmt.Errorf("get security group: %w", err) } else if resp == nil { return "", ErrResponseNil - } else if resp.Name == nil { - return "", ErrNameNil } - return *resp.Name, nil + return resp.Name, nil } -func GetPublicIP(ctx context.Context, apiClient IaaSClient, projectId, publicIpId string) (ip, associatedResource string, err error) { - resp, err := apiClient.GetPublicIPExecute(ctx, projectId, publicIpId) +func GetPublicIP(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, publicIpId string) (ip, associatedResource string, err error) { + resp, err := apiClient.GetPublicIP(ctx, projectId, region, publicIpId).Execute() if err != nil { return "", "", fmt.Errorf("get public ip: %w", err) } associatedResourceId := "" - if resp.NetworkInterface != nil { + if resp.NetworkInterface.IsSet() { associatedResourceId = *resp.NetworkInterface.Get() } return *resp.Ip, associatedResourceId, nil } -func GetServerName(ctx context.Context, apiClient IaaSClient, projectId, serverId string) (string, error) { - resp, err := apiClient.GetServerExecute(ctx, projectId, serverId) +func GetServerName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, serverId string) (string, error) { + resp, err := apiClient.GetServer(ctx, projectId, region, serverId).Execute() if err != nil { return "", fmt.Errorf("get server: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetVolumeName(ctx context.Context, apiClient IaaSClient, projectId, volumeId string) (string, error) { - resp, err := apiClient.GetVolumeExecute(ctx, projectId, volumeId) +func GetVolumeName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, volumeId string) (string, error) { + resp, err := apiClient.GetVolume(ctx, projectId, region, volumeId).Execute() if err != nil { return "", fmt.Errorf("get volume: %w", err) } else if resp == nil { @@ -83,32 +65,38 @@ func GetVolumeName(ctx context.Context, apiClient IaaSClient, projectId, volumeI return *resp.Name, nil } -func GetNetworkName(ctx context.Context, apiClient IaaSClient, projectId, networkId string) (string, error) { - resp, err := apiClient.GetNetworkExecute(ctx, projectId, networkId) +func GetNetworkName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, networkId string) (string, error) { + resp, err := apiClient.GetNetwork(ctx, projectId, region, networkId).Execute() if err != nil { return "", fmt.Errorf("get network: %w", err) } else if resp == nil { return "", ErrResponseNil - } else if resp.Name == nil { - return "", ErrNameNil } - return *resp.Name, nil + return resp.Name, nil } -func GetNetworkAreaName(ctx context.Context, apiClient IaaSClient, organizationId, areaId string) (string, error) { - resp, err := apiClient.GetNetworkAreaExecute(ctx, organizationId, areaId) +func GetRoutingTableOfAreaName(ctx context.Context, apiClient iaas.DefaultAPI, organizationId, areaId, region, routingTableId string) (string, error) { + resp, err := apiClient.GetRoutingTableOfArea(ctx, organizationId, areaId, region, routingTableId).Execute() + if err != nil { + return "", fmt.Errorf("get routing-table: %w", err) + } else if resp == nil { + return "", ErrResponseNil + } + return resp.Name, nil +} + +func GetNetworkAreaName(ctx context.Context, apiClient iaas.DefaultAPI, organizationId, areaId string) (string, error) { + resp, err := apiClient.GetNetworkArea(ctx, organizationId, areaId).Execute() if err != nil { return "", fmt.Errorf("get network area: %w", err) } else if resp == nil { return "", ErrResponseNil - } else if resp.Name == nil { - return "", ErrNameNil } - return *resp.Name, nil + return resp.Name, nil } -func ListAttachedProjects(ctx context.Context, apiClient IaaSClient, organizationId, areaId string) ([]string, error) { - resp, err := apiClient.ListNetworkAreaProjectsExecute(ctx, organizationId, areaId) +func ListAttachedProjects(ctx context.Context, apiClient iaas.DefaultAPI, organizationId, areaId string) ([]string, error) { + resp, err := apiClient.ListNetworkAreaProjects(ctx, organizationId, areaId).Execute() if err != nil { return nil, fmt.Errorf("list network area attached projects: %w", err) } else if resp == nil { @@ -116,22 +104,41 @@ func ListAttachedProjects(ctx context.Context, apiClient IaaSClient, organizatio } else if resp.Items == nil { return nil, ErrItemsNil } - return *resp.Items, nil + return resp.Items, nil } -func GetNetworkRangePrefix(ctx context.Context, apiClient IaaSClient, organizationId, areaId, networkRangeId string) (string, error) { - resp, err := apiClient.GetNetworkAreaRangeExecute(ctx, organizationId, areaId, networkRangeId) +func GetNetworkRangePrefix(ctx context.Context, apiClient iaas.DefaultAPI, organizationId, areaId, region, networkRangeId string) (string, error) { + resp, err := apiClient.GetNetworkAreaRange(ctx, organizationId, areaId, region, networkRangeId).Execute() if err != nil { return "", fmt.Errorf("get network range: %w", err) } - return *resp.Prefix, nil + return resp.Prefix, nil } // GetRouteFromAPIResponse returns the static route from the API response that matches the prefix and nexthop // This works because static routes are unique by prefix and nexthop -func GetRouteFromAPIResponse(prefix, nexthop string, routes *[]iaas.Route) (iaas.Route, error) { - for _, route := range *routes { - if *route.Prefix == prefix && *route.Nexthop == nexthop { +func GetRouteFromAPIResponse(destination, nexthop string, routes []iaas.Route) (iaas.Route, error) { + for _, route := range routes { + // Check if destination matches + destV4 := route.Destination.DestinationCIDRv4 + destV4Matches := destV4 != nil && destV4.Value == destination + destV6 := route.Destination.DestinationCIDRv6 + destV6Matches := destV6 != nil && destV6.Value == destination + destMatches := destV4Matches || destV6Matches + if !destMatches { + continue + } + // Check if nexthop matches + nextHopV4 := route.Nexthop.NexthopIPv4 + nextHopV4Matches := nextHopV4 != nil && nextHopV4.Value == nexthop + nextHopV6 := route.Nexthop.NexthopIPv6 + nextHopV6Matches := nextHopV6 != nil && nextHopV6.Value == nexthop + nextHopInet := route.Nexthop.NexthopInternet + nextHopInetMatches := nextHopInet != nil && nextHopInet.Type == nexthop + nextHopBlackhole := route.Nexthop.NexthopBlackhole + nextHopBlackholeMatches := nextHopBlackhole != nil && nextHopBlackhole.Type == nexthop + nextHopMatches := nextHopV4Matches || nextHopV6Matches || nextHopInetMatches || nextHopBlackholeMatches + if nextHopMatches { return route, nil } } @@ -140,41 +147,37 @@ func GetRouteFromAPIResponse(prefix, nexthop string, routes *[]iaas.Route) (iaas // GetNetworkRangeFromAPIResponse returns the network range from the API response that matches the given prefix // This works because network range prefixes are unique in the same SNA -func GetNetworkRangeFromAPIResponse(prefix string, networkRanges *[]iaas.NetworkRange) (iaas.NetworkRange, error) { - for _, networkRange := range *networkRanges { - if *networkRange.Prefix == prefix { +func GetNetworkRangeFromAPIResponse(prefix string, networkRanges []iaas.NetworkRange) (iaas.NetworkRange, error) { + for _, networkRange := range networkRanges { + if networkRange.Prefix == prefix { return networkRange, nil } } return iaas.NetworkRange{}, fmt.Errorf("new network range not found in API response") } -func GetImageName(ctx context.Context, apiClient IaaSClient, projectId, imageId string) (string, error) { - resp, err := apiClient.GetImageExecute(ctx, projectId, imageId) +func GetImageName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, imageId string) (string, error) { + resp, err := apiClient.GetImage(ctx, projectId, region, imageId).Execute() if err != nil { return "", fmt.Errorf("get image: %w", err) } else if resp == nil { return "", ErrResponseNil - } else if resp.Name == nil { - return "", ErrNameNil } - return *resp.Name, nil + return resp.Name, nil } -func GetAffinityGroupName(ctx context.Context, apiClient IaaSClient, projectId, affinityGroupId string) (string, error) { - resp, err := apiClient.GetAffinityGroupExecute(ctx, projectId, affinityGroupId) +func GetAffinityGroupName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, affinityGroupId string) (string, error) { + resp, err := apiClient.GetAffinityGroup(ctx, projectId, region, affinityGroupId).Execute() if err != nil { return "", fmt.Errorf("get affinity group: %w", err) } else if resp == nil { return "", ErrResponseNil - } else if resp.Name == nil { - return "", ErrNameNil } - return *resp.Name, nil + return resp.Name, nil } -func GetSnapshotName(ctx context.Context, apiClient IaaSClient, projectId, snapshotId string) (string, error) { - resp, err := apiClient.GetSnapshotExecute(ctx, projectId, snapshotId) +func GetSnapshotName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, snapshotId string) (string, error) { + resp, err := apiClient.GetSnapshot(ctx, projectId, region, snapshotId).Execute() if err != nil { return "", fmt.Errorf("get snapshot: %w", err) } else if resp == nil { @@ -185,8 +188,8 @@ func GetSnapshotName(ctx context.Context, apiClient IaaSClient, projectId, snaps return *resp.Name, nil } -func GetBackupName(ctx context.Context, apiClient IaaSClient, projectId, backupId string) (string, error) { - resp, err := apiClient.GetBackupExecute(ctx, projectId, backupId) +func GetBackupName(ctx context.Context, apiClient iaas.DefaultAPI, projectId, region, backupId string) (string, error) { + resp, err := apiClient.GetBackup(ctx, projectId, region, backupId).Execute() if err != nil { return backupId, fmt.Errorf("get backup: %w", err) } diff --git a/internal/pkg/services/iaas/utils/utils_test.go b/internal/pkg/services/iaas/utils/utils_test.go index e2ccdc469..bb27d10cf 100644 --- a/internal/pkg/services/iaas/utils/utils_test.go +++ b/internal/pkg/services/iaas/utils/utils_test.go @@ -6,129 +6,131 @@ import ( "reflect" "testing" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) type IaaSClientMocked struct { - GetSecurityGroupRuleFails bool - GetSecurityGroupRuleResp *iaas.SecurityGroupRule - GetSecurityGroupFails bool - GetSecurityGroupResp *iaas.SecurityGroup - GetPublicIpFails bool - GetPublicIpResp *iaas.PublicIp - GetServerFails bool - GetServerResp *iaas.Server - GetVolumeFails bool - GetVolumeResp *iaas.Volume - GetNetworkFails bool - GetNetworkResp *iaas.Network - GetNetworkAreaFails bool - GetNetworkAreaResp *iaas.NetworkArea - GetAttachedProjectsFails bool - GetAttachedProjectsResp *iaas.ProjectListResponse - GetNetworkAreaRangeFails bool - GetNetworkAreaRangeResp *iaas.NetworkRange - GetImageFails bool - GetImageResp *iaas.Image - GetAffinityGroupsFails bool - GetAffinityGroupResp *iaas.AffinityGroup - GetBackupFails bool - GetBackupResp *iaas.Backup - GetSnapshotFails bool - GetSnapshotResp *iaas.Snapshot -} - -func (m *IaaSClientMocked) GetAffinityGroupExecute(_ context.Context, _, _ string) (*iaas.AffinityGroup, error) { - if m.GetAffinityGroupsFails { - return nil, fmt.Errorf("could not get affinity groups") - } - return m.GetAffinityGroupResp, nil -} - -func (m *IaaSClientMocked) GetSecurityGroupRuleExecute(_ context.Context, _, _, _ string) (*iaas.SecurityGroupRule, error) { - if m.GetSecurityGroupRuleFails { - return nil, fmt.Errorf("could not get security group rule") - } - return m.GetSecurityGroupRuleResp, nil -} - -func (m *IaaSClientMocked) GetSecurityGroupExecute(_ context.Context, _, _ string) (*iaas.SecurityGroup, error) { - if m.GetSecurityGroupFails { - return nil, fmt.Errorf("could not get security group") - } - return m.GetSecurityGroupResp, nil -} - -func (m *IaaSClientMocked) GetPublicIPExecute(_ context.Context, _, _ string) (*iaas.PublicIp, error) { - if m.GetPublicIpFails { - return nil, fmt.Errorf("could not get public ip") - } - return m.GetPublicIpResp, nil -} - -func (m *IaaSClientMocked) GetServerExecute(_ context.Context, _, _ string) (*iaas.Server, error) { - if m.GetServerFails { - return nil, fmt.Errorf("could not get server") - } - return m.GetServerResp, nil -} - -func (m *IaaSClientMocked) GetVolumeExecute(_ context.Context, _, _ string) (*iaas.Volume, error) { - if m.GetVolumeFails { - return nil, fmt.Errorf("could not get volume") - } - return m.GetVolumeResp, nil -} - -func (m *IaaSClientMocked) GetNetworkExecute(_ context.Context, _, _ string) (*iaas.Network, error) { - if m.GetNetworkFails { - return nil, fmt.Errorf("could not get network") - } - return m.GetNetworkResp, nil + GetSecurityGroupRuleFails bool + GetSecurityGroupRuleResp *iaas.SecurityGroupRule + GetSecurityGroupFails bool + GetSecurityGroupResp *iaas.SecurityGroup + GetPublicIpFails bool + GetPublicIpResp *iaas.PublicIp + GetServerFails bool + GetServerResp *iaas.Server + GetVolumeFails bool + GetVolumeResp *iaas.Volume + GetNetworkFails bool + GetNetworkResp *iaas.Network + GetRoutingTableOfAreaFails bool + GetRoutingTableOfAreaResp *iaas.RoutingTable + GetNetworkAreaFails bool + GetNetworkAreaResp *iaas.NetworkArea + GetAttachedProjectsFails bool + GetAttachedProjectsResp *iaas.ProjectListResponse + GetNetworkAreaRangeFails bool + GetNetworkAreaRangeResp *iaas.NetworkRange + GetImageFails bool + GetImageResp *iaas.Image + GetAffinityGroupsFails bool + GetAffinityGroupResp *iaas.AffinityGroup + GetBackupFails bool + GetBackupResp *iaas.Backup + GetSnapshotFails bool + GetSnapshotResp *iaas.Snapshot } -func (m *IaaSClientMocked) GetNetworkAreaExecute(_ context.Context, _, _ string) (*iaas.NetworkArea, error) { - if m.GetNetworkAreaFails { - return nil, fmt.Errorf("could not get network area") - } - return m.GetNetworkAreaResp, nil -} - -func (m *IaaSClientMocked) ListNetworkAreaProjectsExecute(_ context.Context, _, _ string) (*iaas.ProjectListResponse, error) { - if m.GetAttachedProjectsFails { - return nil, fmt.Errorf("could not get attached projects") - } - return m.GetAttachedProjectsResp, nil -} - -func (m *IaaSClientMocked) GetNetworkAreaRangeExecute(_ context.Context, _, _, _ string) (*iaas.NetworkRange, error) { - if m.GetNetworkAreaRangeFails { - return nil, fmt.Errorf("could not get network range") - } - return m.GetNetworkAreaRangeResp, nil -} - -func (m *IaaSClientMocked) GetImageExecute(_ context.Context, _, _ string) (*iaas.Image, error) { - if m.GetImageFails { - return nil, fmt.Errorf("could not get image") - } - return m.GetImageResp, nil -} - -func (m *IaaSClientMocked) GetBackupExecute(_ context.Context, _, _ string) (*iaas.Backup, error) { - if m.GetBackupFails { - return nil, fmt.Errorf("could not get backup") +func newMock(m *IaaSClientMocked) iaas.DefaultAPI { + return iaas.DefaultAPIServiceMock{ + GetAffinityGroupExecuteMock: utils.Ptr(func(_ iaas.ApiGetAffinityGroupRequest) (*iaas.AffinityGroup, error) { + if m.GetAffinityGroupsFails { + return nil, fmt.Errorf("could not get affinity groups") + } + return m.GetAffinityGroupResp, nil + }), + GetSecurityGroupRuleExecuteMock: utils.Ptr(func(_ iaas.ApiGetSecurityGroupRuleRequest) (*iaas.SecurityGroupRule, error) { + if m.GetSecurityGroupRuleFails { + return nil, fmt.Errorf("could not get security group rule") + } + return m.GetSecurityGroupRuleResp, nil + }), + GetSecurityGroupExecuteMock: utils.Ptr(func(_ iaas.ApiGetSecurityGroupRequest) (*iaas.SecurityGroup, error) { + if m.GetSecurityGroupFails { + return nil, fmt.Errorf("could not get security group") + } + return m.GetSecurityGroupResp, nil + }), + GetPublicIPExecuteMock: utils.Ptr(func(_ iaas.ApiGetPublicIPRequest) (*iaas.PublicIp, error) { + if m.GetPublicIpFails { + return nil, fmt.Errorf("could not get public ip") + } + return m.GetPublicIpResp, nil + }), + GetServerExecuteMock: utils.Ptr(func(_ iaas.ApiGetServerRequest) (*iaas.Server, error) { + if m.GetServerFails { + return nil, fmt.Errorf("could not get server") + } + return m.GetServerResp, nil + }), + GetVolumeExecuteMock: utils.Ptr(func(_ iaas.ApiGetVolumeRequest) (*iaas.Volume, error) { + if m.GetVolumeFails { + return nil, fmt.Errorf("could not get volume") + } + return m.GetVolumeResp, nil + }), + GetNetworkExecuteMock: utils.Ptr(func(_ iaas.ApiGetNetworkRequest) (*iaas.Network, error) { + if m.GetNetworkFails { + return nil, fmt.Errorf("could not get network") + } + return m.GetNetworkResp, nil + }), + GetRoutingTableOfAreaExecuteMock: utils.Ptr(func(_ iaas.ApiGetRoutingTableOfAreaRequest) (*iaas.RoutingTable, error) { + if m.GetRoutingTableOfAreaFails { + return nil, fmt.Errorf("could not get routing table") + } + return m.GetRoutingTableOfAreaResp, nil + }), + GetNetworkAreaExecuteMock: utils.Ptr(func(_ iaas.ApiGetNetworkAreaRequest) (*iaas.NetworkArea, error) { + if m.GetNetworkAreaFails { + return nil, fmt.Errorf("could not get network area") + } + return m.GetNetworkAreaResp, nil + }), + ListNetworkAreaProjectsExecuteMock: utils.Ptr(func(_ iaas.ApiListNetworkAreaProjectsRequest) (*iaas.ProjectListResponse, error) { + if m.GetAttachedProjectsFails { + return nil, fmt.Errorf("could not get attached projects") + } + return m.GetAttachedProjectsResp, nil + }), + GetNetworkAreaRangeExecuteMock: utils.Ptr(func(_ iaas.ApiGetNetworkAreaRangeRequest) (*iaas.NetworkRange, error) { + if m.GetNetworkAreaRangeFails { + return nil, fmt.Errorf("could not get network range") + } + return m.GetNetworkAreaRangeResp, nil + }), + GetImageExecuteMock: utils.Ptr(func(_ iaas.ApiGetImageRequest) (*iaas.Image, error) { + if m.GetImageFails { + return nil, fmt.Errorf("could not get image") + } + return m.GetImageResp, nil + }), + GetBackupExecuteMock: utils.Ptr(func(_ iaas.ApiGetBackupRequest) (*iaas.Backup, error) { + if m.GetBackupFails { + return nil, fmt.Errorf("could not get backup") + } + return m.GetBackupResp, nil + }), + GetSnapshotExecuteMock: utils.Ptr(func(_ iaas.ApiGetSnapshotRequest) (*iaas.Snapshot, error) { + if m.GetSnapshotFails { + return nil, fmt.Errorf("could not get snapshot") + } + return m.GetSnapshotResp, nil + }), } - return m.GetBackupResp, nil } -func (m *IaaSClientMocked) GetSnapshotExecute(_ context.Context, _, _ string) (*iaas.Snapshot, error) { - if m.GetSnapshotFails { - return nil, fmt.Errorf("could not get snapshot") - } - return m.GetSnapshotResp, nil -} func TestGetSecurityGroupRuleName(t *testing.T) { type args struct { getInstanceFails bool @@ -145,7 +147,7 @@ func TestGetSecurityGroupRuleName(t *testing.T) { args: args{ getInstanceResp: &iaas.SecurityGroupRule{ Ethertype: utils.Ptr("IPv6"), - Direction: utils.Ptr("ingress"), + Direction: "ingress", }, }, want: "IPv6, ingress", @@ -164,7 +166,7 @@ func TestGetSecurityGroupRuleName(t *testing.T) { GetSecurityGroupRuleFails: tt.args.getInstanceFails, GetSecurityGroupRuleResp: tt.args.getInstanceResp, } - got, err := GetSecurityGroupRuleName(context.Background(), m, "", "", "") + got, err := GetSecurityGroupRuleName(context.Background(), newMock(m), "", "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetSecurityGroupRuleName() error = %v, wantErr %v", err, tt.wantErr) return @@ -191,7 +193,7 @@ func TestGetSecurityGroupName(t *testing.T) { name: "base", args: args{ getInstanceResp: &iaas.SecurityGroup{ - Name: utils.Ptr("test"), + Name: "test", }, }, want: "test", @@ -212,17 +214,6 @@ func TestGetSecurityGroupName(t *testing.T) { wantErr: true, want: "", }, - { - name: "name in response is nil", - args: args{ - getInstanceResp: &iaas.SecurityGroup{ - Name: nil, - }, - getInstanceFails: false, - }, - wantErr: true, - want: "", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -230,7 +221,7 @@ func TestGetSecurityGroupName(t *testing.T) { GetSecurityGroupFails: tt.args.getInstanceFails, GetSecurityGroupResp: tt.args.getInstanceResp, } - got, err := GetSecurityGroupName(context.Background(), m, "", "") + got, err := GetSecurityGroupName(context.Background(), newMock(m), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetSecurityGroupName() error = %v, wantErr %v", err, tt.wantErr) return @@ -259,7 +250,7 @@ func TestGetPublicIp(t *testing.T) { args: args{ getPublicIpResp: &iaas.PublicIp{ Ip: utils.Ptr("1.2.3.4"), - NetworkInterface: iaas.NewNullableString(utils.Ptr("5.6.7.8")), + NetworkInterface: *iaas.NewNullableString(utils.Ptr("5.6.7.8")), }, }, wantPublicIp: "1.2.3.4", @@ -279,7 +270,7 @@ func TestGetPublicIp(t *testing.T) { GetPublicIpFails: tt.args.getPublicIpFails, GetPublicIpResp: tt.args.getPublicIpResp, } - gotPublicIP, gotAssociatedResource, err := GetPublicIP(context.Background(), m, "", "") + gotPublicIP, gotAssociatedResource, err := GetPublicIP(context.Background(), newMock(m), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetPublicIP() error = %v, wantErr %v", err, tt.wantErr) return @@ -309,7 +300,7 @@ func TestGetServerName(t *testing.T) { name: "base", args: args{ getInstanceResp: &iaas.Server{ - Name: utils.Ptr("test"), + Name: "test", }, }, want: "test", @@ -328,7 +319,7 @@ func TestGetServerName(t *testing.T) { GetServerFails: tt.args.getInstanceFails, GetServerResp: tt.args.getInstanceResp, } - got, err := GetServerName(context.Background(), m, "", "") + got, err := GetServerName(context.Background(), newMock(m), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetServerName() error = %v, wantErr %v", err, tt.wantErr) return @@ -377,7 +368,7 @@ func TestGetVolumeName(t *testing.T) { want: "", }, { - name: "name in response is nil", + name: "name in response is empty", args: args{ getInstanceResp: &iaas.Volume{ Name: nil, @@ -394,7 +385,7 @@ func TestGetVolumeName(t *testing.T) { GetVolumeFails: tt.args.getInstanceFails, GetVolumeResp: tt.args.getInstanceResp, } - got, err := GetVolumeName(context.Background(), m, "", "") + got, err := GetVolumeName(context.Background(), newMock(m), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetVolumeName() error = %v, wantErr %v", err, tt.wantErr) return @@ -421,7 +412,7 @@ func TestGetNetworkName(t *testing.T) { name: "base", args: args{ getInstanceResp: &iaas.Network{ - Name: utils.Ptr("test"), + Name: "test", }, }, want: "test", @@ -442,17 +433,6 @@ func TestGetNetworkName(t *testing.T) { wantErr: true, want: "", }, - { - name: "name in response is nil", - args: args{ - getInstanceResp: &iaas.Network{ - Name: nil, - }, - getInstanceFails: false, - }, - wantErr: true, - want: "", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -460,7 +440,7 @@ func TestGetNetworkName(t *testing.T) { GetNetworkFails: tt.args.getInstanceFails, GetNetworkResp: tt.args.getInstanceResp, } - got, err := GetNetworkName(context.Background(), m, "", "") + got, err := GetNetworkName(context.Background(), newMock(m), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetNetworkName() error = %v, wantErr %v", err, tt.wantErr) return @@ -487,7 +467,7 @@ func TestGetNetworkAreaName(t *testing.T) { name: "base", args: args{ getInstanceResp: &iaas.NetworkArea{ - Name: utils.Ptr("test"), + Name: "test", }, }, want: "test", @@ -509,17 +489,6 @@ func TestGetNetworkAreaName(t *testing.T) { wantErr: true, want: "", }, - { - name: "name in response is nil", - args: args{ - getInstanceResp: &iaas.NetworkArea{ - Name: nil, - }, - getInstanceFails: false, - }, - wantErr: true, - want: "", - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -527,7 +496,7 @@ func TestGetNetworkAreaName(t *testing.T) { GetNetworkAreaFails: tt.args.getInstanceFails, GetNetworkAreaResp: tt.args.getInstanceResp, } - got, err := GetNetworkAreaName(context.Background(), m, "", "") + got, err := GetNetworkAreaName(context.Background(), newMock(m), "", "") if (err != nil) != tt.wantErr { t.Errorf("GetNetworkAreaName() error = %v, wantErr %v", err, tt.wantErr) return @@ -554,7 +523,7 @@ func TestListAttachedProjects(t *testing.T) { name: "base", args: args{ getAttachedProjectsResp: &iaas.ProjectListResponse{ - Items: &[]string{"test"}, + Items: []string{"test"}, }, }, want: []string{"test"}, @@ -573,7 +542,7 @@ func TestListAttachedProjects(t *testing.T) { GetAttachedProjectsFails: tt.args.getAttachedProjectsFails, GetAttachedProjectsResp: tt.args.getAttachedProjectsResp, } - got, err := ListAttachedProjects(context.Background(), m, "", "") + got, err := ListAttachedProjects(context.Background(), newMock(m), "", "") if (err != nil) != tt.wantErr { t.Errorf("GetAttachedProjects() error = %v, wantErr %v", err, tt.wantErr) return @@ -600,7 +569,7 @@ func TestGetNetworkRangePrefix(t *testing.T) { name: "base", args: args{ getNetworkAreaRangeResp: &iaas.NetworkRange{ - Prefix: utils.Ptr("test"), + Prefix: "test", }, }, want: "test", @@ -619,7 +588,7 @@ func TestGetNetworkRangePrefix(t *testing.T) { GetNetworkAreaRangeFails: tt.args.getNetworkAreaRangeFails, GetNetworkAreaRangeResp: tt.args.getNetworkAreaRangeResp, } - got, err := GetNetworkRangePrefix(context.Background(), m, "", "", "") + got, err := GetNetworkRangePrefix(context.Background(), newMock(m), "", "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetNetworkRangePrefix() error = %v, wantErr %v", err, tt.wantErr) return @@ -635,7 +604,7 @@ func TestGetRouteFromAPIResponse(t *testing.T) { type args struct { prefix string nexthop string - routes *[]iaas.Route + routes []iaas.Route } tests := []struct { name string @@ -648,24 +617,212 @@ func TestGetRouteFromAPIResponse(t *testing.T) { args: args{ prefix: "1.1.1.0/24", nexthop: "1.1.1.1", - routes: &[]iaas.Route{ + routes: []iaas.Route{ + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: "cidrv4", + Value: "1.1.1.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: "ipv4", + Value: "1.1.1.1", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: "cidrv4", + Value: "2.2.2.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: "ipv4", + Value: "2.2.2.2", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "3.3.3.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: "blackhole", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "4.4.4.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: "internet", + }, + }, + }, + }, + }, + want: iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: "cidrv4", + Value: "1.1.1.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: "ipv4", + Value: "1.1.1.1", + }, + }, + }, + }, + { + name: "nexthop internet", + args: args{ + prefix: "4.4.4.0/24", + nexthop: "internet", + routes: []iaas.Route{ { - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "1.1.1.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: "1.1.1.1", + }, + }, }, { - Prefix: utils.Ptr("2.2.2.0/24"), - Nexthop: utils.Ptr("2.2.2.2"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "2.2.2.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: "2.2.2.2", + }, + }, }, { - Prefix: utils.Ptr("3.3.3.0/24"), - Nexthop: utils.Ptr("3.3.3.3"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "3.3.3.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: "blackhole", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "4.4.4.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: "internet", + }, + }, }, }, }, want: iaas.Route{ - Prefix: utils.Ptr("1.1.1.0/24"), - Nexthop: utils.Ptr("1.1.1.1"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "4.4.4.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: "internet", + }, + }, + }, + }, + { + name: "nexthop blackhole", + args: args{ + prefix: "3.3.3.0/24", + nexthop: "blackhole", + routes: []iaas.Route{ + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "1.1.1.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: "1.1.1.1", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "2.2.2.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: "2.2.2.2", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "3.3.3.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: "blackhole", + }, + }, + }, + { + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "4.4.4.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: "internet", + }, + }, + }, + }, + }, + want: iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "3.3.3.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: "blackhole", + }, + }, }, }, { @@ -673,14 +830,30 @@ func TestGetRouteFromAPIResponse(t *testing.T) { args: args{ prefix: "1.1.1.0/24", nexthop: "1.1.1.1", - routes: &[]iaas.Route{ + routes: []iaas.Route{ { - Prefix: utils.Ptr("2.2.2.0/24"), - Nexthop: utils.Ptr("2.2.2.2"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "2.2.2.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: "2.2.2.2", + }, + }, }, { - Prefix: utils.Ptr("3.3.3.0/24"), - Nexthop: utils.Ptr("3.3.3.3"), + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Value: "3.3.3.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Value: "3.3.3.3", + }, + }, }, }, }, @@ -691,7 +864,7 @@ func TestGetRouteFromAPIResponse(t *testing.T) { args: args{ prefix: "1.1.1.0/24", nexthop: "1.1.1.1", - routes: &[]iaas.Route{}, + routes: []iaas.Route{}, }, wantErr: true, }, @@ -713,7 +886,7 @@ func TestGetRouteFromAPIResponse(t *testing.T) { func TestGetNetworkRangeFromAPIResponse(t *testing.T) { type args struct { prefix string - networkRanges *[]iaas.NetworkRange + networkRanges []iaas.NetworkRange } tests := []struct { name string @@ -725,32 +898,32 @@ func TestGetNetworkRangeFromAPIResponse(t *testing.T) { name: "base", args: args{ prefix: "1.1.1.0/24", - networkRanges: &[]iaas.NetworkRange{ + networkRanges: []iaas.NetworkRange{ { - Prefix: utils.Ptr("1.1.1.0/24"), + Prefix: "1.1.1.0/24", }, { - Prefix: utils.Ptr("2.2.2.0/24"), + Prefix: "2.2.2.0/24", }, { - Prefix: utils.Ptr("3.3.3.0/24"), + Prefix: "3.3.3.0/24", }, }, }, want: iaas.NetworkRange{ - Prefix: utils.Ptr("1.1.1.0/24"), + Prefix: "1.1.1.0/24", }, }, { name: "not found", args: args{ prefix: "1.1.1.0/24", - networkRanges: &[]iaas.NetworkRange{ + networkRanges: []iaas.NetworkRange{ { - Prefix: utils.Ptr("2.2.2.0/24"), + Prefix: "2.2.2.0/24", }, { - Prefix: utils.Ptr("3.3.3.0/24"), + Prefix: "3.3.3.0/24", }, }, }, @@ -760,7 +933,7 @@ func TestGetNetworkRangeFromAPIResponse(t *testing.T) { name: "empty", args: args{ prefix: "1.1.1.0/24", - networkRanges: &[]iaas.NetworkRange{}, + networkRanges: []iaas.NetworkRange{}, }, wantErr: true, }, @@ -789,7 +962,7 @@ func TestGetImageName(t *testing.T) { }{ { name: "successful retrieval", - imageResp: &iaas.Image{Name: utils.Ptr("test-image")}, + imageResp: &iaas.Image{Name: "test-image"}, want: "test-image", wantErr: false, }, @@ -805,13 +978,6 @@ func TestGetImageName(t *testing.T) { want: "", wantErr: true, }, - { - name: "name in response is nil", - imageErr: false, - imageResp: &iaas.Image{Name: nil}, - want: "", - wantErr: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -819,7 +985,7 @@ func TestGetImageName(t *testing.T) { GetImageFails: tt.imageErr, GetImageResp: tt.imageResp, } - got, err := GetImageName(context.Background(), client, "", "") + got, err := GetImageName(context.Background(), newMock(client), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetImageName() error = %v, wantErr %v", err, tt.wantErr) return @@ -841,7 +1007,7 @@ func TestGetAffinityGroupName(t *testing.T) { }{ { name: "successful retrieval", - affinityResp: &iaas.AffinityGroup{Name: utils.Ptr("test-affinity")}, + affinityResp: &iaas.AffinityGroup{Name: "test-affinity"}, want: "test-affinity", wantErr: false, }, @@ -851,22 +1017,11 @@ func TestGetAffinityGroupName(t *testing.T) { wantErr: true, }, { - name: "response is nil", - affinityErr: false, - affinityResp: &iaas.AffinityGroup{ - Name: nil, - }, - want: "", - wantErr: true, - }, - { - name: "affinity group name in response is nil", - affinityErr: false, - affinityResp: &iaas.AffinityGroup{ - Name: nil, - }, - want: "", - wantErr: true, + name: "response is nil", + affinityErr: false, + affinityResp: nil, + want: "", + wantErr: true, }, } for _, tt := range tests { @@ -876,7 +1031,7 @@ func TestGetAffinityGroupName(t *testing.T) { GetAffinityGroupsFails: tt.affinityErr, GetAffinityGroupResp: tt.affinityResp, } - got, err := GetAffinityGroupName(ctx, client, "", "") + got, err := GetAffinityGroupName(ctx, newMock(client), "", "", "") if (err != nil) != tt.wantErr { t.Errorf("GetAffinityGroupName() error = %v, wantErr %v", err, tt.wantErr) return @@ -887,3 +1042,60 @@ func TestGetAffinityGroupName(t *testing.T) { }) } } + +func TestGetRoutingTableOfAreaName(t *testing.T) { + type args struct { + getInstanceFails bool + getInstanceResp *iaas.RoutingTable + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "base", + args: args{ + getInstanceResp: &iaas.RoutingTable{ + Name: "test", + }, + }, + want: "test", + }, + { + name: "get routing table fails", + args: args{ + getInstanceFails: true, + }, + wantErr: true, + want: "", + }, + { + name: "response is nil", + args: args{ + getInstanceResp: nil, + getInstanceFails: false, + }, + wantErr: true, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &IaaSClientMocked{ + GetRoutingTableOfAreaFails: tt.args.getInstanceFails, + GetRoutingTableOfAreaResp: tt.args.getInstanceResp, + } + + got, err := GetRoutingTableOfAreaName(context.Background(), newMock(m), "", "", "", "") + if (err != nil) != tt.wantErr { + t.Errorf("GetRoutingTableOfAreaName() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetRoutingTableOfAreaName() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/services/intake/client/client.go b/internal/pkg/services/intake/client/client.go new file mode 100644 index 000000000..7f2495356 --- /dev/null +++ b/internal/pkg/services/intake/client/client.go @@ -0,0 +1,15 @@ +package client + +import ( + "github.com/spf13/viper" + intake "github.com/stackitcloud/stackit-sdk-go/services/intake/v1betaapi" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +// ConfigureClient creates and configures a new Intake API client +func ConfigureClient(p *print.Printer, cliVersion string) (*intake.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.IntakeCustomEndpointKey), false, genericclient.CreateApiClient[*intake.APIClient](intake.NewAPIClient)) +} diff --git a/internal/pkg/services/kms/client/client.go b/internal/pkg/services/kms/client/client.go new file mode 100644 index 000000000..7dc5dc29d --- /dev/null +++ b/internal/pkg/services/kms/client/client.go @@ -0,0 +1,14 @@ +package client + +import ( + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/viper" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*kms.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.KMSCustomEndpointKey), false, kms.NewAPIClient) +} diff --git a/internal/pkg/services/kms/utils/utils.go b/internal/pkg/services/kms/utils/utils.go new file mode 100644 index 000000000..c0b303b94 --- /dev/null +++ b/internal/pkg/services/kms/utils/utils.go @@ -0,0 +1,61 @@ +package utils + +import ( + "context" + "fmt" + "time" + + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" +) + +func GetKeyName(ctx context.Context, apiClient kms.DefaultAPI, projectId, region, keyRingId, keyId string) (string, error) { + resp, err := apiClient.GetKey(ctx, projectId, region, keyRingId, keyId).Execute() + if err != nil { + return "", fmt.Errorf("get KMS Key: %w", err) + } + + if resp == nil { + return "", fmt.Errorf("response is nil / empty") + } + + return resp.DisplayName, nil +} + +func GetKeyDeletionDate(ctx context.Context, apiClient kms.DefaultAPI, projectId, region, keyRingId, keyId string) (time.Time, error) { + resp, err := apiClient.GetKey(ctx, projectId, region, keyRingId, keyId).Execute() + if err != nil { + return time.Now(), fmt.Errorf("get KMS Key: %w", err) + } + + if resp == nil || resp.DeletionDate == nil { + return time.Time{}, fmt.Errorf("response is nil / empty") + } + + return *resp.DeletionDate, nil +} + +func GetKeyRingName(ctx context.Context, apiClient kms.DefaultAPI, projectId, id, region string) (string, error) { + resp, err := apiClient.GetKeyRing(ctx, projectId, region, id).Execute() + if err != nil { + return "", fmt.Errorf("get KMS key ring: %w", err) + } + + if resp == nil { + return "", fmt.Errorf("response is nil / empty") + } + + return resp.DisplayName, nil +} + +func GetWrappingKeyName(ctx context.Context, apiClient kms.DefaultAPI, projectId, region, keyRingId, wrappingKeyId string) (string, error) { + resp, err := apiClient.GetWrappingKey(ctx, projectId, region, keyRingId, wrappingKeyId).Execute() + if err != nil { + return "", fmt.Errorf("get KMS Wrapping Key: %w", err) + } + + if resp == nil { + return "", fmt.Errorf("response is nil / empty") + } + + return resp.DisplayName, nil +} diff --git a/internal/pkg/services/kms/utils/utils_test.go b/internal/pkg/services/kms/utils/utils_test.go new file mode 100644 index 000000000..365d4ef30 --- /dev/null +++ b/internal/pkg/services/kms/utils/utils_test.go @@ -0,0 +1,260 @@ +package utils + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + kms "github.com/stackitcloud/stackit-sdk-go/services/kms/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +var ( + testProjectId = uuid.NewString() + testKeyRingId = uuid.NewString() + testKeyId = uuid.NewString() + testWrappingKeyId = uuid.NewString() +) + +const ( + testRegion = "eu01" + testKeyName = "my-test-key" + testKeyRingName = "my-key-ring" + testWrappingKeyName = "my-wrapping-key" +) + +type kmsClientMocked struct { + getKeyFails bool + getKeyResp *kms.Key + getKeyRingFails bool + getKeyRingResp *kms.KeyRing + getWrappingKeyFails bool + getWrappingKeyResp *kms.WrappingKey +} + +func (m *kmsClientMocked) newMock() kms.DefaultAPI { + return kms.DefaultAPIServiceMock{ + GetKeyExecuteMock: utils.Ptr(func(_ kms.ApiGetKeyRequest) (*kms.Key, error) { + if m.getKeyFails { + return nil, fmt.Errorf("could not get key") + } + return m.getKeyResp, nil + }), + GetKeyRingExecuteMock: utils.Ptr(func(_ kms.ApiGetKeyRingRequest) (*kms.KeyRing, error) { + if m.getKeyRingFails { + return nil, fmt.Errorf("could not get key ring") + } + return m.getKeyRingResp, nil + }), + GetWrappingKeyExecuteMock: utils.Ptr(func(_ kms.ApiGetWrappingKeyRequest) (*kms.WrappingKey, error) { + if m.getWrappingKeyFails { + return nil, fmt.Errorf("could not get wrapping key") + } + return m.getWrappingKeyResp, nil + }), + } +} + +func TestGetKeyName(t *testing.T) { + keyName := testKeyName + + tests := []struct { + description string + getKeyFails bool + getKeyResp *kms.Key + isValid bool + expectedOutput string + }{ + { + description: "base", + getKeyResp: &kms.Key{ + DisplayName: keyName, + }, + isValid: true, + expectedOutput: testKeyName, + }, + { + description: "get key fails", + getKeyFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &kmsClientMocked{ + getKeyFails: tt.getKeyFails, + getKeyResp: tt.getKeyResp, + } + + output, err := GetKeyName(context.Background(), client.newMock(), testProjectId, testRegion, testKeyRingId, testKeyId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input: %v", err) + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %q, got %q", tt.expectedOutput, output) + } + }) + } +} + +// TestGetKeyDeletionDate tests the GetKeyDeletionDate function. +func TestGetKeyDeletionDate(t *testing.T) { + mockTime := time.Date(2025, 8, 20, 0, 0, 0, 0, time.UTC) + + tests := []struct { + description string + getKeyFails bool + getKeyResp *kms.Key + isValid bool + expectedOutput time.Time + }{ + { + description: "base", + getKeyResp: &kms.Key{ + DeletionDate: &mockTime, + }, + isValid: true, + expectedOutput: mockTime, + }, + { + description: "get key fails", + getKeyFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &kmsClientMocked{ + getKeyFails: tt.getKeyFails, + getKeyResp: tt.getKeyResp, + } + + output, err := GetKeyDeletionDate(context.Background(), client.newMock(), testProjectId, testRegion, testKeyRingId, testKeyId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input: %v", err) + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if !output.Equal(tt.expectedOutput) { + t.Errorf("expected output to be %v, got %v", tt.expectedOutput, output) + } + }) + } +} + +// TestGetKeyRingName tests the GetKeyRingName function. +func TestGetKeyRingName(t *testing.T) { + keyRingName := testKeyRingName + + tests := []struct { + description string + getKeyRingFails bool + getKeyRingResp *kms.KeyRing + isValid bool + expectedOutput string + }{ + { + description: "base", + getKeyRingResp: &kms.KeyRing{ + DisplayName: keyRingName, + }, + isValid: true, + expectedOutput: testKeyRingName, + }, + { + description: "get key ring fails", + getKeyRingFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &kmsClientMocked{ + getKeyRingFails: tt.getKeyRingFails, + getKeyRingResp: tt.getKeyRingResp, + } + + output, err := GetKeyRingName(context.Background(), client.newMock(), testProjectId, testKeyRingId, testRegion) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input: %v", err) + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %q, got %q", tt.expectedOutput, output) + } + }) + } +} + +func TestGetWrappingKeyName(t *testing.T) { + wrappingKeyName := testWrappingKeyName + tests := []struct { + description string + getWrappingKeyFails bool + getWrappingKeyResp *kms.WrappingKey + isValid bool + expectedOutput string + }{ + { + description: "base", + getWrappingKeyResp: &kms.WrappingKey{ + DisplayName: wrappingKeyName, + }, + isValid: true, + expectedOutput: testWrappingKeyName, + }, + { + description: "get wrapping key fails", + getWrappingKeyFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &kmsClientMocked{ + getWrappingKeyFails: tt.getWrappingKeyFails, + getWrappingKeyResp: tt.getWrappingKeyResp, + } + + output, err := GetWrappingKeyName(context.Background(), client.newMock(), testProjectId, testRegion, testKeyRingId, testWrappingKeyId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input: %v", err) + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %q, got %q", tt.expectedOutput, output) + } + }) + } +} diff --git a/internal/pkg/services/load-balancer/client/client.go b/internal/pkg/services/load-balancer/client/client.go index 402498c70..273bea02d 100644 --- a/internal/pkg/services/load-balancer/client/client.go +++ b/internal/pkg/services/load-balancer/client/client.go @@ -1,44 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*loadbalancer.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.LoadBalancerCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := loadbalancer.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.LoadBalancerCustomEndpointKey), false, loadbalancer.NewAPIClient) } diff --git a/internal/pkg/services/load-balancer/utils/utils.go b/internal/pkg/services/load-balancer/utils/utils.go index 4bc13db61..68fd2fd2f 100644 --- a/internal/pkg/services/load-balancer/utils/utils.go +++ b/internal/pkg/services/load-balancer/utils/utils.go @@ -6,7 +6,7 @@ import ( "slices" "sort" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" ) const ( @@ -15,28 +15,16 @@ const ( OP_FILTER_UNUSED ) -// enforce implementation of interfaces -var ( - _ LoadBalancerClient = &loadbalancer.APIClient{} -) - -type LoadBalancerClient interface { - GetCredentialsExecute(ctx context.Context, projectId, region, credentialsRef string) (*loadbalancer.GetCredentialsResponse, error) - GetLoadBalancerExecute(ctx context.Context, projectId, region, name string) (*loadbalancer.LoadBalancer, error) - UpdateTargetPool(ctx context.Context, projectId, region, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest - ListLoadBalancersExecute(ctx context.Context, projectId, region string) (*loadbalancer.ListLoadBalancersResponse, error) -} - -func GetCredentialsDisplayName(ctx context.Context, apiClient LoadBalancerClient, projectId, region, credentialsRef string) (string, error) { - resp, err := apiClient.GetCredentialsExecute(ctx, projectId, region, credentialsRef) +func GetCredentialsDisplayName(ctx context.Context, apiClient loadbalancer.DefaultAPI, projectId, region, credentialsRef string) (string, error) { + resp, err := apiClient.GetCredentials(ctx, projectId, region, credentialsRef).Execute() if err != nil { return "", fmt.Errorf("get Load Balancer credentials: %w", err) } return *resp.Credential.DisplayName, nil } -func GetLoadBalancerTargetPool(ctx context.Context, apiClient LoadBalancerClient, projectId, region, loadBalancerName, targetPoolName string) (*loadbalancer.TargetPool, error) { - resp, err := apiClient.GetLoadBalancerExecute(ctx, projectId, region, loadBalancerName) +func GetLoadBalancerTargetPool(ctx context.Context, apiClient loadbalancer.DefaultAPI, projectId, region, loadBalancerName, targetPoolName string) (*loadbalancer.TargetPool, error) { + resp, err := apiClient.GetLoadBalancer(ctx, projectId, region, loadBalancerName).Execute() if err != nil { return nil, fmt.Errorf("get load balancer: %w", err) } @@ -48,7 +36,7 @@ func GetLoadBalancerTargetPool(ctx context.Context, apiClient LoadBalancerClient return nil, fmt.Errorf("no target pools found") } - targetPool := FindLoadBalancerTargetPoolByName(*resp.TargetPools, targetPoolName) + targetPool := FindLoadBalancerTargetPoolByName(resp.TargetPools, targetPoolName) if targetPool == nil { return nil, fmt.Errorf("target pool not found") } @@ -87,10 +75,10 @@ func AddTargetToTargetPool(targetPool *loadbalancer.TargetPool, target *loadbala return fmt.Errorf("target is nil") } if targetPool.Targets == nil { - targetPool.Targets = &[]loadbalancer.Target{*target} + targetPool.Targets = []loadbalancer.Target{*target} return nil } - *targetPool.Targets = append(*targetPool.Targets, *target) + targetPool.Targets = append(targetPool.Targets, *target) return nil } @@ -101,12 +89,12 @@ func RemoveTargetFromTargetPool(targetPool *loadbalancer.TargetPool, ip string) if targetPool.Targets == nil { return fmt.Errorf("no targets found") } - targets := *targetPool.Targets + targets := targetPool.Targets for i, target := range targets { if target.Ip != nil && *target.Ip == ip { newTargets := targets[:i] newTargets = append(newTargets, targets[i+1:]...) - *targetPool.Targets = newTargets + targetPool.Targets = newTargets return nil } } @@ -126,7 +114,7 @@ func ToPayloadTargetPool(targetPool *loadbalancer.TargetPool) *loadbalancer.Upda } } -func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, region, loadBalancerName, targetPoolName, targetIp string) (string, error) { +func GetTargetName(ctx context.Context, apiClient loadbalancer.DefaultAPI, projectId, region, loadBalancerName, targetPoolName, targetIp string) (string, error) { targetPool, err := GetLoadBalancerTargetPool(ctx, apiClient, projectId, region, loadBalancerName, targetPoolName) if err != nil { return "", fmt.Errorf("get target pool: %w", err) @@ -134,7 +122,7 @@ func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, if targetPool.Targets == nil { return "", fmt.Errorf("no targets found") } - for _, target := range *targetPool.Targets { + for _, target := range targetPool.Targets { if target.Ip != nil && *target.Ip == targetIp { if target.DisplayName == nil { return "", fmt.Errorf("nil target display name") @@ -147,10 +135,9 @@ func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, // GetUsedObsCredentials returns a list of credentials that are used by load balancers for observability metrics or logs. // It goes through all load balancers and checks what observability credentials are being used, then returns a list of those credentials. -func GetUsedObsCredentials(ctx context.Context, apiClient LoadBalancerClient, allCredentials []loadbalancer.CredentialsResponse, projectId, region string) ([]loadbalancer.CredentialsResponse, error) { +func GetUsedObsCredentials(ctx context.Context, apiClient loadbalancer.DefaultAPI, allCredentials []loadbalancer.CredentialsResponse, projectId, region string) ([]loadbalancer.CredentialsResponse, error) { var usedCredentialsSlice []loadbalancer.CredentialsResponse - - loadBalancers, err := apiClient.ListLoadBalancersExecute(ctx, projectId, region) + loadBalancers, err := apiClient.ListLoadBalancers(ctx, projectId, region).Execute() if err != nil { return nil, fmt.Errorf("list load balancers: %w", err) } @@ -159,7 +146,9 @@ func GetUsedObsCredentials(ctx context.Context, apiClient LoadBalancerClient, al } var usedCredentialsRefs []string - for _, loadBalancer := range *loadBalancers.LoadBalancers { + for i := range loadBalancers.LoadBalancers { + loadBalancer := &(loadBalancers.LoadBalancers)[i] + if loadBalancer.Options == nil || loadBalancer.Options.Observability == nil { continue } @@ -223,7 +212,7 @@ func GetUnusedObsCredentials(usedCredentials, allCredentials []loadbalancer.Cred // If unused is true, it returns only the credentials that are not used by any load balancer for observability metrics or logs. // If both used and unused are true, it returns an error. // If both used and unused are false, it returns the original list of credentials. -func FilterCredentials(ctx context.Context, client LoadBalancerClient, allCredentials []loadbalancer.CredentialsResponse, projectId, region string, filterOp int) ([]loadbalancer.CredentialsResponse, error) { +func FilterCredentials(ctx context.Context, client loadbalancer.DefaultAPI, allCredentials []loadbalancer.CredentialsResponse, projectId, region string, filterOp int) ([]loadbalancer.CredentialsResponse, error) { // check that filter OP is valid if filterOp != OP_FILTER_USED && filterOp != OP_FILTER_UNUSED && filterOp != OP_FILTER_NOP { return nil, fmt.Errorf("invalid filter operation") @@ -232,7 +221,6 @@ func FilterCredentials(ctx context.Context, client LoadBalancerClient, allCreden if filterOp == OP_FILTER_NOP { return allCredentials, nil } - usedCredentials, err := GetUsedObsCredentials(ctx, client, allCredentials, projectId, region) if err != nil { return nil, fmt.Errorf("get used observability credentials: %w", err) diff --git a/internal/pkg/services/load-balancer/utils/utils_test.go b/internal/pkg/services/load-balancer/utils/utils_test.go index 5941b2c93..942c886bf 100644 --- a/internal/pkg/services/load-balancer/utils/utils_test.go +++ b/internal/pkg/services/load-balancer/utils/utils_test.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + loadbalancer "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/v2api" "github.com/google/go-cmp/cmp" "github.com/google/uuid" @@ -25,7 +25,7 @@ const ( testLoadBalancerName = "my-load-balancer" ) -type loadBalancerClientMocked struct { +type mockSettings struct { getCredentialsFails bool getCredentialsResp *loadbalancer.GetCredentialsResponse getLoadBalancerFails bool @@ -34,38 +34,36 @@ type loadBalancerClientMocked struct { listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse } -func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get credentials") - } - return m.getCredentialsResp, nil -} - -func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, _, _ string) (*loadbalancer.LoadBalancer, error) { - if m.getLoadBalancerFails { - return nil, fmt.Errorf("could not get load balancer") - } - return m.getLoadBalancerResp, nil -} - -func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { - if m.listLoadBalancersFails { - return nil, fmt.Errorf("could not list load balancers") +func newAPIMock(s mockSettings) loadbalancer.DefaultAPI { + return &loadbalancer.DefaultAPIServiceMock{ + GetCredentialsExecuteMock: utils.Ptr(func(_ loadbalancer.ApiGetCredentialsRequest) (*loadbalancer.GetCredentialsResponse, error) { + if s.getCredentialsFails { + return nil, fmt.Errorf("could not get credentials") + } + return s.getCredentialsResp, nil + }), + GetLoadBalancerExecuteMock: utils.Ptr(func(_ loadbalancer.ApiGetLoadBalancerRequest) (*loadbalancer.LoadBalancer, error) { + if s.getLoadBalancerFails { + return nil, fmt.Errorf("could not get load balancer") + } + return s.getLoadBalancerResp, nil + }), + ListLoadBalancersExecuteMock: utils.Ptr(func(_ loadbalancer.ApiListLoadBalancersRequest) (*loadbalancer.ListLoadBalancersResponse, error) { + if s.listLoadBalancersFails { + return nil, fmt.Errorf("could not list load balancers") + } + return s.listLoadBalancersResp, nil + }), } - return m.listLoadBalancersResp, nil -} - -func (m *loadBalancerClientMocked) UpdateTargetPool(_ context.Context, _, _, _, _ string) loadbalancer.ApiUpdateTargetPoolRequest { - return loadbalancer.UpdateTargetPoolRequest{} } func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer.LoadBalancer { lb := loadbalancer.LoadBalancer{ Name: utils.Ptr(testLoadBalancerName), - TargetPools: &[]loadbalancer.TargetPool{ + TargetPools: []loadbalancer.TargetPool{ { Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -78,7 +76,7 @@ func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer }, { Name: utils.Ptr("target-pool-2"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("6.7.8.9"), @@ -136,8 +134,8 @@ func fixtureCredentials(mod ...func([]loadbalancer.CredentialsResponse)) []loadb return credentials } -func fixtureTargets(mod ...func(*[]loadbalancer.Target)) *[]loadbalancer.Target { - targets := &[]loadbalancer.Target{ +func fixtureTargets(mod ...func([]loadbalancer.Target)) []loadbalancer.Target { + targets := []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -186,12 +184,12 @@ func TestGetCredentialsDisplayName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ getCredentialsFails: tt.getCredentialsFails, getCredentialsResp: tt.getCredentialsResp, } - output, err := GetCredentialsDisplayName(context.Background(), client, testProjectId, testRegion, testCredentialsRef) + output, err := GetCredentialsDisplayName(context.Background(), newAPIMock(client), testProjectId, testRegion, testCredentialsRef) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -225,7 +223,7 @@ func TestGetLoadBalancerTargetPool(t *testing.T) { isValid: true, expectedOutput: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -246,7 +244,7 @@ func TestGetLoadBalancerTargetPool(t *testing.T) { { description: "no target pools", getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - lb.TargetPools = &[]loadbalancer.TargetPool{} + lb.TargetPools = []loadbalancer.TargetPool{} }), isValid: false, }, @@ -266,12 +264,12 @@ func TestGetLoadBalancerTargetPool(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ getLoadBalancerFails: tt.getLoadBalancerFails, getLoadBalancerResp: tt.getLoadBalancerResp, } - output, err := GetLoadBalancerTargetPool(context.Background(), client, testProjectId, testRegion, testLoadBalancerName, tt.targetPoolName) + output, err := GetLoadBalancerTargetPool(context.Background(), newAPIMock(client), testProjectId, testRegion, testLoadBalancerName, tt.targetPoolName) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -424,7 +422,7 @@ func TestAddTargetToTargetPool(t *testing.T) { description: "base", targetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -438,7 +436,7 @@ func TestAddTargetToTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -454,7 +452,7 @@ func TestAddTargetToTargetPool(t *testing.T) { description: "no target pool targets", targetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{}, + Targets: []loadbalancer.Target{}, }, target: &loadbalancer.Target{ DisplayName: utils.Ptr("target-3"), @@ -463,7 +461,7 @@ func TestAddTargetToTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-3"), Ip: utils.Ptr("2.2.2.2"), @@ -484,7 +482,7 @@ func TestAddTargetToTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-3"), Ip: utils.Ptr("2.2.2.2"), @@ -505,7 +503,7 @@ func TestAddTargetToTargetPool(t *testing.T) { description: "nil new target", targetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -556,7 +554,7 @@ func TestRemoveTargetFromTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-2"), Ip: utils.Ptr("2.2.2.2"), @@ -578,7 +576,7 @@ func TestRemoveTargetFromTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -600,7 +598,7 @@ func TestRemoveTargetFromTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -616,7 +614,7 @@ func TestRemoveTargetFromTargetPool(t *testing.T) { description: "remove only target", targetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -627,14 +625,14 @@ func TestRemoveTargetFromTargetPool(t *testing.T) { isValid: true, expectedTargetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{}, + Targets: []loadbalancer.Target{}, }, }, { description: "no target pool targets", targetPool: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{}, + Targets: []loadbalancer.Target{}, }, targetIp: "2.2.2.2", isValid: false, @@ -688,13 +686,13 @@ func TestToPayloadTargetPool(t *testing.T) { input: &loadbalancer.TargetPool{ Name: utils.Ptr("target-pool-1"), ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - UnhealthyThreshold: utils.Ptr(int64(3)), + UnhealthyThreshold: utils.Ptr(int32(3)), }, SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(true), }, - TargetPort: utils.Ptr(int64(80)), - Targets: &[]loadbalancer.Target{ + TargetPort: utils.Ptr(int32(80)), + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -704,13 +702,13 @@ func TestToPayloadTargetPool(t *testing.T) { expected: &loadbalancer.UpdateTargetPoolPayload{ Name: utils.Ptr("target-pool-1"), ActiveHealthCheck: &loadbalancer.ActiveHealthCheck{ - UnhealthyThreshold: utils.Ptr(int64(3)), + UnhealthyThreshold: utils.Ptr(int32(3)), }, SessionPersistence: &loadbalancer.SessionPersistence{ UseSourceIpAddress: utils.Ptr(true), }, - TargetPort: utils.Ptr(int64(80)), - Targets: &[]loadbalancer.Target{ + TargetPort: utils.Ptr(int32(80)), + Targets: []loadbalancer.Target{ { DisplayName: utils.Ptr("target-1"), Ip: utils.Ptr("1.2.3.4"), @@ -767,10 +765,10 @@ func TestGetTargetName(t *testing.T) { targetPoolName: "target-pool-1", targetIp: "1.2.3.4", getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - lb.TargetPools = &[]loadbalancer.TargetPool{ + lb.TargetPools = []loadbalancer.TargetPool{ { Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{}, + Targets: []loadbalancer.Target{}, }, } }), @@ -781,7 +779,7 @@ func TestGetTargetName(t *testing.T) { targetPoolName: "target-pool-1", targetIp: "1.2.3.4", getLoadBalancerResp: fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { - lb.TargetPools = &[]loadbalancer.TargetPool{ + lb.TargetPools = []loadbalancer.TargetPool{ { Name: utils.Ptr("target-pool-1"), Targets: nil, @@ -796,10 +794,10 @@ func TestGetTargetName(t *testing.T) { targetIp: "1.2.3.4", getLoadBalancerResp: fixtureLoadBalancer( func(lb *loadbalancer.LoadBalancer) { - lb.TargetPools = &[]loadbalancer.TargetPool{ + lb.TargetPools = []loadbalancer.TargetPool{ { Name: utils.Ptr("target-pool-1"), - Targets: &[]loadbalancer.Target{ + Targets: []loadbalancer.Target{ { DisplayName: nil, Ip: utils.Ptr("1.2.3.4"), @@ -821,11 +819,11 @@ func TestGetTargetName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ getLoadBalancerResp: tt.getLoadBalancerResp, } - output, err := GetTargetName(context.Background(), client, testProjectId, testRegion, testLoadBalancerName, tt.targetPoolName, tt.targetIp) + output, err := GetTargetName(context.Background(), newAPIMock(client), testProjectId, testRegion, testLoadBalancerName, tt.targetPoolName, tt.targetIp) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -856,7 +854,7 @@ func TestGetUsedObsCredentials(t *testing.T) { description: "base", allCredentials: fixtureCredentials(), listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(), }, }, @@ -878,7 +876,7 @@ func TestGetUsedObsCredentials(t *testing.T) { description: "repeated credentials in different load balancers", allCredentials: fixtureCredentials(), listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(), *fixtureLoadBalancer(), }, @@ -901,7 +899,7 @@ func TestGetUsedObsCredentials(t *testing.T) { description: "no repeated credentials in different load balancers", allCredentials: fixtureCredentials(), listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(), *fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { lb.Options.Observability.Logs.CredentialsRef = utils.Ptr("credentials-ref-3") @@ -929,7 +927,7 @@ func TestGetUsedObsCredentials(t *testing.T) { description: "no credentials", allCredentials: []loadbalancer.CredentialsResponse{}, listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(), }, }, @@ -945,7 +943,7 @@ func TestGetUsedObsCredentials(t *testing.T) { description: "no observability options", allCredentials: fixtureCredentials(), listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { lb.Options = nil }), @@ -958,12 +956,12 @@ func TestGetUsedObsCredentials(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ listLoadBalancersFails: tt.listLoadBalancersFails, listLoadBalancersResp: tt.listLoadBalancersResp, } - output, err := GetUsedObsCredentials(testCtx, client, tt.allCredentials, testProjectId, testRegion) + output, err := GetUsedObsCredentials(testCtx, newAPIMock(client), tt.allCredentials, testProjectId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -1077,7 +1075,7 @@ func TestFilterCredentials(t *testing.T) { filterOp: OP_FILTER_USED, allCredentials: fixtureCredentials(), listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(), }, }, @@ -1100,7 +1098,7 @@ func TestFilterCredentials(t *testing.T) { filterOp: OP_FILTER_UNUSED, allCredentials: fixtureCredentials(), listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ - LoadBalancers: &[]loadbalancer.LoadBalancer{ + LoadBalancers: []loadbalancer.LoadBalancer{ *fixtureLoadBalancer(), }, }, @@ -1136,11 +1134,11 @@ func TestFilterCredentials(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &loadBalancerClientMocked{ + client := mockSettings{ listLoadBalancersResp: tt.listLoadBalancersResp, listLoadBalancersFails: tt.listLoadBalancersFails, } - filteredCredentials, err := FilterCredentials(testCtx, client, tt.allCredentials, testProjectId, testRegion, tt.filterOp) + filteredCredentials, err := FilterCredentials(testCtx, newAPIMock(client), tt.allCredentials, testProjectId, testRegion, tt.filterOp) if err != nil { if !tt.isValid { return diff --git a/internal/pkg/services/logme/client/client.go b/internal/pkg/services/logme/client/client.go index 71a750c7f..e07801cb8 100644 --- a/internal/pkg/services/logme/client/client.go +++ b/internal/pkg/services/logme/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*logme.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.LogMeCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := logme.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.LogMeCustomEndpointKey), true, logme.NewAPIClient) } diff --git a/internal/pkg/services/logme/utils/utils.go b/internal/pkg/services/logme/utils/utils.go index d63b4ea5d..072f6bafd 100644 --- a/internal/pkg/services/logme/utils/utils.go +++ b/internal/pkg/services/logme/utils/utils.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) const ( @@ -15,9 +15,9 @@ const ( ) func ValidatePlanId(planId string, offerings *logme.ListOfferingsResponse) error { - for _, offer := range *offerings.Offerings { - for _, plan := range *offer.Plans { - if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + for _, offer := range offerings.Offerings { + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Id, planId) { return nil } } @@ -33,21 +33,18 @@ func LoadPlanId(planName, version string, offerings *logme.ListOfferingsResponse availableVersions := "" availablePlanNames := "" isValidVersion := false - for _, offer := range *offerings.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) + for _, offer := range offerings.Offerings { + if !strings.EqualFold(offer.Version, version) { + availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, offer.Version) continue } isValidVersion = true - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Name, planName) { + return &plan.Id, nil } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - return plan.Id, nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, plan.Name) } } @@ -65,23 +62,18 @@ func LoadPlanId(planName, version string, offerings *logme.ListOfferingsResponse } } -type LogMeClient interface { - GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*logme.Instance, error) - GetCredentialsExecute(ctx context.Context, projectId, instanceId, credentialsId string) (*logme.CredentialsResponse, error) -} - -func GetInstanceName(ctx context.Context, apiClient LogMeClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) +func GetInstanceName(ctx context.Context, apiClient logme.DefaultAPI, projectId, instanceId string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { return "", fmt.Errorf("get LogMe instance: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetCredentialsUsername(ctx context.Context, apiClient LogMeClient, projectId, instanceId, credentialsId string) (string, error) { - resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) +func GetCredentialsUsername(ctx context.Context, apiClient logme.DefaultAPI, projectId, instanceId, credentialsId string) (string, error) { + resp, err := apiClient.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { return "", fmt.Errorf("get LogMe credentials: %w", err) } - return *resp.Raw.Credentials.Username, nil + return resp.Raw.Credentials.Username, nil } diff --git a/internal/pkg/services/logme/utils/utils_test.go b/internal/pkg/services/logme/utils/utils_test.go index 97fde111f..b9384e29a 100644 --- a/internal/pkg/services/logme/utils/utils_test.go +++ b/internal/pkg/services/logme/utils/utils_test.go @@ -8,7 +8,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/logme" + logme "github.com/stackitcloud/stackit-sdk-go/services/logme/v1api" ) var ( @@ -22,25 +22,28 @@ const ( testCredentialsUsername = "username" ) -type logMeClientMocked struct { +type mockSettings struct { getInstanceFails bool getInstanceResp *logme.Instance getCredentialsFails bool getCredentialsResp *logme.CredentialsResponse } -func (m *logMeClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*logme.Instance, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *logMeClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*logme.CredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get user") +func newAPIMock(s mockSettings) logme.DefaultAPI { + return &logme.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ logme.ApiGetInstanceRequest) (*logme.Instance, error) { + if s.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return s.getInstanceResp, nil + }), + GetCredentialsExecuteMock: utils.Ptr(func(_ logme.ApiGetCredentialsRequest) (*logme.CredentialsResponse, error) { + if s.getCredentialsFails { + return nil, fmt.Errorf("could not get user") + } + return s.getCredentialsResp, nil + }), } - return m.getCredentialsResp, nil } func TestGetInstanceName(t *testing.T) { @@ -54,7 +57,7 @@ func TestGetInstanceName(t *testing.T) { { description: "base", getInstanceResp: &logme.Instance{ - Name: utils.Ptr(testInstanceName), + Name: testInstanceName, }, isValid: true, expectedOutput: testInstanceName, @@ -68,12 +71,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &logMeClientMocked{ + client := mockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + output, err := GetInstanceName(context.Background(), newAPIMock(client), testProjectId, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -103,8 +106,8 @@ func TestGetCredentialsUsername(t *testing.T) { description: "base", getCredentialsResp: &logme.CredentialsResponse{ Raw: &logme.RawCredentials{ - Credentials: &logme.Credentials{ - Username: utils.Ptr(testCredentialsUsername), + Credentials: logme.Credentials{ + Username: testCredentialsUsername, }, }, }, @@ -120,12 +123,12 @@ func TestGetCredentialsUsername(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &logMeClientMocked{ + client := mockSettings{ getCredentialsFails: tt.getCredentialsFails, getCredentialsResp: tt.getCredentialsResp, } - output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + output, err := GetCredentialsUsername(context.Background(), newAPIMock(client), testProjectId, testInstanceId, testCredentialsId) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/logs/client/client.go b/internal/pkg/services/logs/client/client.go new file mode 100644 index 000000000..fa8abe391 --- /dev/null +++ b/internal/pkg/services/logs/client/client.go @@ -0,0 +1,15 @@ +package client + +import ( + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/viper" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*logs.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.LogsCustomEndpointKey), false, logs.NewAPIClient) +} diff --git a/internal/pkg/services/logs/utils/utils.go b/internal/pkg/services/logs/utils/utils.go new file mode 100644 index 000000000..16186d618 --- /dev/null +++ b/internal/pkg/services/logs/utils/utils.go @@ -0,0 +1,33 @@ +package utils + +import ( + "context" + "errors" + "fmt" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" +) + +var ( + ErrResponseNil = errors.New("response is nil") +) + +func GetInstanceName(ctx context.Context, apiClient logs.DefaultAPI, projectId, regionId, instanceId string) (string, error) { + resp, err := apiClient.GetLogsInstance(ctx, projectId, regionId, instanceId).Execute() + if err != nil { + return "", fmt.Errorf("get Logs instance: %w", err) + } else if resp == nil { + return "", ErrResponseNil + } + return resp.DisplayName, nil +} + +func GetAccessTokenName(ctx context.Context, apiClient logs.DefaultAPI, projectId, regionId, instanceId, accessTokenId string) (string, error) { + resp, err := apiClient.GetAccessToken(ctx, projectId, regionId, instanceId, accessTokenId).Execute() + if err != nil { + return "", fmt.Errorf("get Logs access token: %w", err) + } else if resp == nil { + return "", ErrResponseNil + } + return resp.DisplayName, nil +} diff --git a/internal/pkg/services/logs/utils/utils_test.go b/internal/pkg/services/logs/utils/utils_test.go new file mode 100644 index 000000000..f2c98d12c --- /dev/null +++ b/internal/pkg/services/logs/utils/utils_test.go @@ -0,0 +1,156 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + logs "github.com/stackitcloud/stackit-sdk-go/services/logs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/uuid" +) + +var ( + testProjectId = uuid.NewString() + testInstanceId = uuid.NewString() + testAccessTokenId = uuid.NewString() +) + +const ( + testInstanceName = "instance" + testRegion = "eu01" +) + +type mockSettings struct { + getInstanceFails bool + getInstanceResp *logs.LogsInstance + getAccessTokenFails bool + getAccessTokenResp *logs.AccessToken +} + +func newAPIMock(s mockSettings) logs.DefaultAPI { + return &logs.DefaultAPIServiceMock{ + GetLogsInstanceExecuteMock: utils.Ptr(func(_ logs.ApiGetLogsInstanceRequest) (*logs.LogsInstance, error) { + if s.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return s.getInstanceResp, nil + }), + GetAccessTokenExecuteMock: utils.Ptr(func(_ logs.ApiGetAccessTokenRequest) (*logs.AccessToken, error) { + if s.getAccessTokenFails { + return nil, fmt.Errorf("could not get access token") + } + return s.getAccessTokenResp, nil + }), + } +} + +func TestGetInstanceName(t *testing.T) { + tests := []struct { + description string + getInstanceFails bool + getInstanceResp *logs.LogsInstance + isValid bool + expectedOutput string + }{ + { + description: "base", + getInstanceResp: &logs.LogsInstance{ + DisplayName: testInstanceName, + }, + isValid: true, + expectedOutput: testInstanceName, + }, + { + description: "get instance fails", + getInstanceFails: true, + isValid: false, + }, + { + description: "response is nil", + getInstanceFails: false, + getInstanceResp: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + settings := mockSettings{ + getInstanceFails: tt.getInstanceFails, + getInstanceResp: tt.getInstanceResp, + } + + output, err := GetInstanceName(context.Background(), newAPIMock(settings), testProjectId, testRegion, testInstanceId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} + +func TestGetAccessTokenName(t *testing.T) { + tests := []struct { + description string + getAccessTokenFails bool + getAccessTokenResp *logs.AccessToken + isValid bool + expectedOutput string + }{ + { + description: "base", + getAccessTokenResp: &logs.AccessToken{ + DisplayName: testInstanceName, + }, + isValid: true, + expectedOutput: testInstanceName, + }, + { + description: "get instance fails", + getAccessTokenFails: true, + isValid: false, + }, + { + description: "response is nil", + getAccessTokenFails: false, + getAccessTokenResp: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + settings := mockSettings{ + getAccessTokenFails: tt.getAccessTokenFails, + getAccessTokenResp: tt.getAccessTokenResp, + } + + output, err := GetAccessTokenName(context.Background(), newAPIMock(settings), testProjectId, testRegion, testInstanceId, testAccessTokenId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} diff --git a/internal/pkg/services/mariadb/client/client.go b/internal/pkg/services/mariadb/client/client.go index 7a7fb9f78..dcbb70f74 100644 --- a/internal/pkg/services/mariadb/client/client.go +++ b/internal/pkg/services/mariadb/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*mariadb.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.MariaDBCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := mariadb.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.MariaDBCustomEndpointKey), true, mariadb.NewAPIClient) } diff --git a/internal/pkg/services/mariadb/utils/utils.go b/internal/pkg/services/mariadb/utils/utils.go index 44872397f..03dd0404f 100644 --- a/internal/pkg/services/mariadb/utils/utils.go +++ b/internal/pkg/services/mariadb/utils/utils.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) const ( @@ -15,9 +15,9 @@ const ( ) func ValidatePlanId(planId string, offerings *mariadb.ListOfferingsResponse) error { - for _, offer := range *offerings.Offerings { - for _, plan := range *offer.Plans { - if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + for _, offer := range offerings.Offerings { + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Id, planId) { return nil } } @@ -33,21 +33,18 @@ func LoadPlanId(planName, version string, offerings *mariadb.ListOfferingsRespon availableVersions := "" availablePlanNames := "" isValidVersion := false - for _, offer := range *offerings.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) + for _, offer := range offerings.Offerings { + if !strings.EqualFold(offer.Version, version) { + availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, offer.Version) continue } isValidVersion = true - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Name, planName) { + return &plan.Id, nil } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - return plan.Id, nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, plan.Name) } } @@ -65,23 +62,18 @@ func LoadPlanId(planName, version string, offerings *mariadb.ListOfferingsRespon } } -type MariaDBClient interface { - GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*mariadb.Instance, error) - GetCredentialsExecute(ctx context.Context, projectId, instanceId, credentialsId string) (*mariadb.CredentialsResponse, error) -} - -func GetInstanceName(ctx context.Context, apiClient MariaDBClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) +func GetInstanceName(ctx context.Context, apiClient mariadb.DefaultAPI, projectId, instanceId string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { return "", fmt.Errorf("get MariaDB instance: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetCredentialsUsername(ctx context.Context, apiClient MariaDBClient, projectId, instanceId, credentialsId string) (string, error) { - resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) +func GetCredentialsUsername(ctx context.Context, apiClient mariadb.DefaultAPI, projectId, instanceId, credentialsId string) (string, error) { + resp, err := apiClient.GetCredentials(ctx, projectId, instanceId, credentialsId).Execute() if err != nil { return "", fmt.Errorf("get MariaDB credentials: %w", err) } - return *resp.Raw.Credentials.Username, nil + return resp.Raw.Credentials.Username, nil } diff --git a/internal/pkg/services/mariadb/utils/utils_test.go b/internal/pkg/services/mariadb/utils/utils_test.go index cd4f1a162..154b44867 100644 --- a/internal/pkg/services/mariadb/utils/utils_test.go +++ b/internal/pkg/services/mariadb/utils/utils_test.go @@ -8,7 +8,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + mariadb "github.com/stackitcloud/stackit-sdk-go/services/mariadb/v1api" ) var ( @@ -22,25 +22,28 @@ const ( testCredentialsUsername = "username" ) -type mariaDBClientMocked struct { +type mockSettings struct { getInstanceFails bool getInstanceResp *mariadb.Instance getCredentialsFails bool getCredentialsResp *mariadb.CredentialsResponse } -func (m *mariaDBClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*mariadb.Instance, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *mariaDBClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*mariadb.CredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get user") +func newAPIMock(m mockSettings) mariadb.DefaultAPI { + return &mariadb.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ mariadb.ApiGetInstanceRequest) (*mariadb.Instance, error) { + if m.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return m.getInstanceResp, nil + }), + GetCredentialsExecuteMock: utils.Ptr(func(_ mariadb.ApiGetCredentialsRequest) (*mariadb.CredentialsResponse, error) { + if m.getCredentialsFails { + return nil, fmt.Errorf("could not get user") + } + return m.getCredentialsResp, nil + }), } - return m.getCredentialsResp, nil } func TestGetInstanceName(t *testing.T) { @@ -54,7 +57,7 @@ func TestGetInstanceName(t *testing.T) { { description: "base", getInstanceResp: &mariadb.Instance{ - Name: utils.Ptr(testInstanceName), + Name: testInstanceName, }, isValid: true, expectedOutput: testInstanceName, @@ -68,12 +71,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mariaDBClientMocked{ + settings := mockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + output, err := GetInstanceName(context.Background(), newAPIMock(settings), testProjectId, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -103,8 +106,8 @@ func TestGetCredentialsUsername(t *testing.T) { description: "base", getCredentialsResp: &mariadb.CredentialsResponse{ Raw: &mariadb.RawCredentials{ - Credentials: &mariadb.Credentials{ - Username: utils.Ptr(testCredentialsUsername), + Credentials: mariadb.Credentials{ + Username: testCredentialsUsername, }, }, }, @@ -120,12 +123,12 @@ func TestGetCredentialsUsername(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mariaDBClientMocked{ + settings := mockSettings{ getCredentialsFails: tt.getCredentialsFails, getCredentialsResp: tt.getCredentialsResp, } - output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + output, err := GetCredentialsUsername(context.Background(), newAPIMock(settings), testProjectId, testInstanceId, testCredentialsId) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/mongodbflex/client/client.go b/internal/pkg/services/mongodbflex/client/client.go index 216fbd9d5..edde69f90 100644 --- a/internal/pkg/services/mongodbflex/client/client.go +++ b/internal/pkg/services/mongodbflex/client/client.go @@ -1,44 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/spf13/viper" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) func ConfigureClient(p *print.Printer, cliVersion string) (*mongodbflex.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.MongoDBFlexCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := mongodbflex.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.MongoDBFlexCustomEndpointKey), false, genericclient.CreateApiClient[*mongodbflex.APIClient](mongodbflex.NewAPIClient)) } diff --git a/internal/pkg/services/mongodbflex/utils/utils.go b/internal/pkg/services/mongodbflex/utils/utils.go index a5cbc7016..8ee805f89 100644 --- a/internal/pkg/services/mongodbflex/utils/utils.go +++ b/internal/pkg/services/mongodbflex/utils/utils.go @@ -7,24 +7,25 @@ import ( "slices" "strings" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "golang.org/x/mod/semver" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) // The number of replicas is enforced by the API according to the instance type -var instanceTypeToReplicas = map[string]int64{ +var instanceTypeToReplicas = map[string]int32{ "Single": 1, "Replica": 3, "Sharded": 9, } type MongoDBFlexClient interface { - ListVersionsExecute(ctx context.Context, projectId, region string) (*mongodbflex.ListVersionsResponse, error) - GetInstanceExecute(ctx context.Context, projectId, instanceId, region string) (*mongodbflex.InstanceResponse, error) - GetUserExecute(ctx context.Context, projectId, instanceId, userId, region string) (*mongodbflex.GetUserResponse, error) - ListRestoreJobsExecute(ctx context.Context, projectId string, instanceId, region string) (*mongodbflex.ListRestoreJobsResponse, error) + ListVersions(ctx context.Context, projectId, region string) mongodbflex.ApiListVersionsRequest + GetInstance(ctx context.Context, projectId, instanceId, region string) mongodbflex.ApiGetInstanceRequest + GetUser(ctx context.Context, projectId, instanceId, userId, region string) mongodbflex.ApiGetUserRequest + ListRestoreJobs(ctx context.Context, projectId string, instanceId, region string) mongodbflex.ApiListRestoreJobsRequest } func AvailableInstanceTypes() []string { @@ -40,7 +41,7 @@ func AvailableInstanceTypes() []string { return instanceTypes } -func GetInstanceReplicas(instanceType string) (int64, error) { +func GetInstanceReplicas(instanceType string) (int32, error) { numReplicas, ok := instanceTypeToReplicas[instanceType] if !ok { return 0, fmt.Errorf("invalid instance type: %v", instanceType) @@ -48,7 +49,7 @@ func GetInstanceReplicas(instanceType string) (int64, error) { return numReplicas, nil } -func GetInstanceType(numReplicas int64) (string, error) { +func GetInstanceType(numReplicas int32) (string, error) { for k, v := range instanceTypeToReplicas { if v == numReplicas { return k, nil @@ -57,12 +58,12 @@ func GetInstanceType(numReplicas int64) (string, error) { return "", fmt.Errorf("invalid number of replicas: %v", numReplicas) } -func ValidateFlavorId(flavorId string, flavors *[]mongodbflex.InstanceFlavor) error { +func ValidateFlavorId(flavorId string, flavors []mongodbflex.InstanceFlavor) error { if flavors == nil { return fmt.Errorf("nil flavors") } - for _, f := range *flavors { + for _, f := range flavors { if f.Id != nil && strings.EqualFold(*f.Id, flavorId) { return nil } @@ -89,7 +90,7 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *mongodb return nil } - for _, sc := range *storages.StorageClasses { + for _, sc := range storages.StorageClasses { if strings.EqualFold(*storageClass, sc) { return nil } @@ -101,7 +102,7 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *mongodb } } -func LoadFlavorId(cpu, ram int64, flavors *[]mongodbflex.InstanceFlavor) (*string, error) { +func LoadFlavorId(cpu, ram int32, flavors *[]mongodbflex.InstanceFlavor) (*string, error) { if flavors == nil { return nil, fmt.Errorf("nil flavors") } @@ -123,20 +124,19 @@ func LoadFlavorId(cpu, ram int64, flavors *[]mongodbflex.InstanceFlavor) (*strin } func GetLatestMongoDBVersion(ctx context.Context, apiClient MongoDBFlexClient, projectId, region string) (string, error) { - resp, err := apiClient.ListVersionsExecute(ctx, projectId, region) + resp, err := apiClient.ListVersions(ctx, projectId, region).Execute() if err != nil { return "", fmt.Errorf("get MongoDB versions: %w", err) } - versions := *resp.Versions latestVersion := "0" - for i := range versions { + for i := range resp.Versions { oldSemVer := fmt.Sprintf("v%s", latestVersion) - newSemVer := fmt.Sprintf("v%s", versions[i]) + newSemVer := fmt.Sprintf("v%s", resp.Versions[i]) if semver.Compare(newSemVer, oldSemVer) != 1 { continue } - latestVersion = versions[i] + latestVersion = resp.Versions[i] } if latestVersion == "0" { return "", fmt.Errorf("no MongoDB versions found") @@ -145,7 +145,7 @@ func GetLatestMongoDBVersion(ctx context.Context, apiClient MongoDBFlexClient, p } func GetInstanceName(ctx context.Context, apiClient MongoDBFlexClient, projectId, instanceId, region string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId, region) + resp, err := apiClient.GetInstance(ctx, projectId, instanceId, region).Execute() if err != nil { return "", fmt.Errorf("get MongoDB Flex instance: %w", err) } @@ -153,7 +153,7 @@ func GetInstanceName(ctx context.Context, apiClient MongoDBFlexClient, projectId } func GetUserName(ctx context.Context, apiClient MongoDBFlexClient, projectId, instanceId, userId, region string) (string, error) { - resp, err := apiClient.GetUserExecute(ctx, projectId, instanceId, userId, region) + resp, err := apiClient.GetUser(ctx, projectId, instanceId, userId, region).Execute() if err != nil { return "", fmt.Errorf("get MongoDB Flex user: %w", err) } @@ -166,7 +166,7 @@ func GetRestoreStatus(backupId string, restoreJobs *mongodbflex.ListRestoreJobsR return state } - restoreJobsSlice := *restoreJobs.Items + restoreJobsSlice := restoreJobs.Items // sort array by descending date slices.SortFunc(restoreJobsSlice, func(i, j mongodbflex.RestoreInstanceStatus) int { @@ -174,7 +174,7 @@ func GetRestoreStatus(backupId string, restoreJobs *mongodbflex.ListRestoreJobsR return cmp.Compare(*j.Date, *i.Date) }) - for _, restoreJob := range *restoreJobs.Items { + for _, restoreJob := range restoreJobs.Items { if *restoreJob.BackupID == backupId { state = *restoreJob.Status break diff --git a/internal/pkg/services/mongodbflex/utils/utils_test.go b/internal/pkg/services/mongodbflex/utils/utils_test.go index 157bc2803..7960bf501 100644 --- a/internal/pkg/services/mongodbflex/utils/utils_test.go +++ b/internal/pkg/services/mongodbflex/utils/utils_test.go @@ -9,7 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + mongodbflex "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/v2api" ) var ( @@ -25,7 +25,36 @@ const ( testUserName = "user" ) -type mongoDBFlexClientMocked struct { +func newAPIClientMock(m clientMockSettings) mongodbflex.DefaultAPI { + return mongodbflex.DefaultAPIServiceMock{ + ListVersionsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListVersionsRequest) (*mongodbflex.ListVersionsResponse, error) { + if m.listVersionsFails { + return nil, fmt.Errorf("could not list versions") + } + return m.listVersionsResp, nil + }), + ListRestoreJobsExecuteMock: utils.Ptr(func(_ mongodbflex.ApiListRestoreJobsRequest) (*mongodbflex.ListRestoreJobsResponse, error) { + if m.listRestoreJobsFails { + return nil, fmt.Errorf("could not list versions") + } + return m.listRestoreJobsResp, nil + }), + GetInstanceExecuteMock: utils.Ptr(func(_ mongodbflex.ApiGetInstanceRequest) (*mongodbflex.InstanceResponse, error) { + if m.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return m.getInstanceResp, nil + }), + GetUserExecuteMock: utils.Ptr(func(_ mongodbflex.ApiGetUserRequest) (*mongodbflex.GetUserResponse, error) { + if m.getUserFails { + return nil, fmt.Errorf("could not get user") + } + return m.getUserResp, nil + }), + } +} + +type clientMockSettings struct { listVersionsFails bool listVersionsResp *mongodbflex.ListVersionsResponse getInstanceFails bool @@ -36,34 +65,6 @@ type mongoDBFlexClientMocked struct { listRestoreJobsResp *mongodbflex.ListRestoreJobsResponse } -func (m *mongoDBFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*mongodbflex.ListVersionsResponse, error) { - if m.listVersionsFails { - return nil, fmt.Errorf("could not list versions") - } - return m.listVersionsResp, nil -} - -func (m *mongoDBFlexClientMocked) ListRestoreJobsExecute(_ context.Context, _, _, _ string) (*mongodbflex.ListRestoreJobsResponse, error) { - if m.listRestoreJobsFails { - return nil, fmt.Errorf("could not list versions") - } - return m.listRestoreJobsResp, nil -} - -func (m *mongoDBFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*mongodbflex.InstanceResponse, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *mongoDBFlexClientMocked) GetUserExecute(_ context.Context, _, _, _, _ string) (*mongodbflex.GetUserResponse, error) { - if m.getUserFails { - return nil, fmt.Errorf("could not get user") - } - return m.getUserResp, nil -} - func TestValidateStorage(t *testing.T) { tests := []struct { description string @@ -77,7 +78,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(10)), storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -97,7 +98,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(1)), storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -110,7 +111,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(200)), storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -123,7 +124,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(5)), storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -136,7 +137,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(20)), storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -149,7 +150,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(10)), storages: &mongodbflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "bar-3"}, + StorageClasses: []string{"bar-1", "bar-2", "bar-3"}, StorageRange: &mongodbflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -176,13 +177,13 @@ func TestValidateFlavorId(t *testing.T) { tests := []struct { description string flavorId string - flavors *[]mongodbflex.InstanceFlavor + flavors []mongodbflex.InstanceFlavor isValid bool }{ { description: "base", flavorId: "foo", - flavors: &[]mongodbflex.InstanceFlavor{ + flavors: []mongodbflex.InstanceFlavor{ {Id: utils.Ptr("bar-1")}, {Id: utils.Ptr("bar-2")}, {Id: utils.Ptr("foo")}, @@ -198,13 +199,13 @@ func TestValidateFlavorId(t *testing.T) { { description: "no flavors", flavorId: "foo", - flavors: &[]mongodbflex.InstanceFlavor{}, + flavors: []mongodbflex.InstanceFlavor{}, isValid: false, }, { description: "nil flavor id", flavorId: "foo", - flavors: &[]mongodbflex.InstanceFlavor{ + flavors: []mongodbflex.InstanceFlavor{ {Id: utils.Ptr("bar-1")}, {Id: nil}, {Id: utils.Ptr("foo")}, @@ -214,7 +215,7 @@ func TestValidateFlavorId(t *testing.T) { { description: "invalid flavor", flavorId: "foo", - flavors: &[]mongodbflex.InstanceFlavor{ + flavors: []mongodbflex.InstanceFlavor{ {Id: utils.Ptr("bar-1")}, {Id: utils.Ptr("bar-2")}, {Id: utils.Ptr("bar-3")}, @@ -239,8 +240,8 @@ func TestValidateFlavorId(t *testing.T) { func TestLoadFlavorId(t *testing.T) { tests := []struct { description string - cpu int64 - ram int64 + cpu int32 + ram int32 flavors *[]mongodbflex.InstanceFlavor isValid bool expectedOutput *string @@ -252,18 +253,18 @@ func TestLoadFlavorId(t *testing.T) { flavors: &[]mongodbflex.InstanceFlavor{ { Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(2)), }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("foo"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, isValid: true, @@ -295,13 +296,13 @@ func TestLoadFlavorId(t *testing.T) { }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("foo"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, isValid: true, @@ -314,18 +315,18 @@ func TestLoadFlavorId(t *testing.T) { flavors: &[]mongodbflex.InstanceFlavor{ { Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(2)), }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, { Id: nil, - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, isValid: false, @@ -337,13 +338,13 @@ func TestLoadFlavorId(t *testing.T) { flavors: &[]mongodbflex.InstanceFlavor{ { Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(2)), }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, }, isValid: false, @@ -386,7 +387,7 @@ func TestGetLatestMongoDBFlexVersion(t *testing.T) { { description: "base", listVersionsResp: &mongodbflex.ListVersionsResponse{ - Versions: &[]string{"8", "10", "9"}, + Versions: []string{"8", "10", "9"}, }, isValid: true, expectedOutput: "10", @@ -399,7 +400,7 @@ func TestGetLatestMongoDBFlexVersion(t *testing.T) { { description: "no versions", listVersionsResp: &mongodbflex.ListVersionsResponse{ - Versions: &[]string{}, + Versions: []string{}, }, isValid: false, }, @@ -407,12 +408,12 @@ func TestGetLatestMongoDBFlexVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mongoDBFlexClientMocked{ + settings := clientMockSettings{ listVersionsFails: tt.listVersionsFails, listVersionsResp: tt.listVersionsResp, } - output, err := GetLatestMongoDBVersion(context.Background(), client, testProjectId, testRegion) + output, err := GetLatestMongoDBVersion(context.Background(), newAPIClientMock(settings), testProjectId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -457,12 +458,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mongoDBFlexClientMocked{ + settings := clientMockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId, testRegion) + output, err := GetInstanceName(context.Background(), newAPIClientMock(settings), testProjectId, testInstanceId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -507,12 +508,12 @@ func TestGetUserName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &mongoDBFlexClientMocked{ + settings := clientMockSettings{ getUserFails: tt.getUserFails, getUserResp: tt.getUserResp, } - output, err := GetUserName(context.Background(), client, testProjectId, testInstanceId, testUserId, testRegion) + output, err := GetUserName(context.Background(), newAPIClientMock(settings), testProjectId, testInstanceId, testUserId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -539,7 +540,7 @@ func TestGetRestoreStatus(t *testing.T) { { description: "base", listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ - Items: &[]mongodbflex.RestoreInstanceStatus{ + Items: []mongodbflex.RestoreInstanceStatus{ { BackupID: utils.Ptr(testBackupId), Date: utils.Ptr("2024-05-14T12:01:11Z"), @@ -557,7 +558,7 @@ func TestGetRestoreStatus(t *testing.T) { { description: "get latest restore, ordered array", listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ - Items: &[]mongodbflex.RestoreInstanceStatus{ + Items: []mongodbflex.RestoreInstanceStatus{ { BackupID: utils.Ptr(testBackupId), Date: utils.Ptr("2024-05-14T12:01:11Z"), @@ -575,7 +576,7 @@ func TestGetRestoreStatus(t *testing.T) { { description: "get latest restore, unordered array", listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ - Items: &[]mongodbflex.RestoreInstanceStatus{ + Items: []mongodbflex.RestoreInstanceStatus{ { BackupID: utils.Ptr(testBackupId), Date: utils.Ptr("2024-05-13T12:01:11Z"), @@ -593,7 +594,7 @@ func TestGetRestoreStatus(t *testing.T) { { description: "get latest restore, another date format", listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ - Items: &[]mongodbflex.RestoreInstanceStatus{ + Items: []mongodbflex.RestoreInstanceStatus{ { BackupID: utils.Ptr(testBackupId), Date: utils.Ptr("2009-11-10 23:00:00 +0000 UTC m=+0.000000001"), @@ -611,7 +612,7 @@ func TestGetRestoreStatus(t *testing.T) { { description: "no restore job for that backup", listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ - Items: &[]mongodbflex.RestoreInstanceStatus{ + Items: []mongodbflex.RestoreInstanceStatus{ { BackupID: utils.Ptr("bar"), Date: utils.Ptr("2024-05-13T12:01:11Z"), @@ -649,7 +650,7 @@ func TestGetRestoreStatus(t *testing.T) { func TestGetInstanceType(t *testing.T) { tests := []struct { description string - numReplicas int64 + numReplicas int32 expectedOutput string isValid bool }{ diff --git a/internal/pkg/services/network-area/routing-table/utils/utils.go b/internal/pkg/services/network-area/routing-table/utils/utils.go new file mode 100644 index 000000000..708c43352 --- /dev/null +++ b/internal/pkg/services/network-area/routing-table/utils/utils.go @@ -0,0 +1,62 @@ +package utils + +import ( + "fmt" + "time" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +type RouteDetails struct { + DestType string + DestValue string + HopType string + HopValue string + CreatedAt string + UpdatedAt string + Labels string +} + +func ExtractRouteDetails(route *iaas.Route) RouteDetails { + var routeDetails RouteDetails + + if route.Destination.DestinationCIDRv4 != nil { + routeDetails.DestType = route.Destination.DestinationCIDRv4.Type + routeDetails.DestValue = route.Destination.DestinationCIDRv4.Value + } else if route.Destination.DestinationCIDRv6 != nil { + routeDetails.DestType = route.Destination.DestinationCIDRv6.Type + routeDetails.DestValue = route.Destination.DestinationCIDRv6.Value + } + + if route.Nexthop.NexthopIPv4 != nil { + routeDetails.HopType = route.Nexthop.NexthopIPv4.Type + routeDetails.HopValue = route.Nexthop.NexthopIPv4.Value + } else if route.Nexthop.NexthopIPv6 != nil { + routeDetails.HopType = route.Nexthop.NexthopIPv6.Type + routeDetails.HopValue = route.Nexthop.NexthopIPv6.Value + } else if route.Nexthop.NexthopInternet != nil { + routeDetails.HopType = route.Nexthop.NexthopInternet.Type + } else if route.Nexthop.NexthopBlackhole != nil { + routeDetails.HopType = route.Nexthop.NexthopBlackhole.Type + } + + if len(route.Labels) > 0 { + stringMap := make(map[string]string) + for key, value := range route.Labels { + stringMap[key] = fmt.Sprintf("%v", value) + } + routeDetails.Labels = utils.JoinStringMap(stringMap, ": ", "\n") + } + + if route.CreatedAt != nil { + routeDetails.CreatedAt = route.CreatedAt.Format(time.RFC3339) + } + + if route.UpdatedAt != nil { + routeDetails.UpdatedAt = route.UpdatedAt.Format(time.RFC3339) + } + + return routeDetails +} diff --git a/internal/pkg/services/network-area/routing-table/utils/utils_test.go b/internal/pkg/services/network-area/routing-table/utils/utils_test.go new file mode 100644 index 000000000..a4128d537 --- /dev/null +++ b/internal/pkg/services/network-area/routing-table/utils/utils_test.go @@ -0,0 +1,266 @@ +package utils + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" +) + +const ipv4 = "ipv4" +const ipv6 = "ipv6" +const cidrv4 = "cidrv4" +const cidrv6 = "cidrv6" + +func TestExtractRouteDetails(t *testing.T) { + created := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC) + updated := time.Date(2024, 1, 2, 4, 5, 6, 0, time.UTC) + + tests := []struct { + description string + input *iaas.Route + want RouteDetails + }{ + { + description: "completely empty route (zero value)", + input: &iaas.Route{}, + want: RouteDetails{}, + }, + { + description: "destination only, no nexthop, no labels", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: cidrv4, + Value: "10.0.0.0/24", + }, + }, + }, + want: RouteDetails{ + DestType: cidrv4, + DestValue: "10.0.0.0/24", + }, + }, + { + description: "nexthop only, no destination, empty labels map", + input: &iaas.Route{ + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: ipv4, + Value: "10.0.0.1", + }, + }, + Labels: map[string]interface{}{}, // empty but non-nil + }, + want: RouteDetails{ + HopType: ipv4, + HopValue: "10.0.0.1", + }, + }, + { + description: "destination present, nexthop struct nil, labels nil", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ + Type: cidrv6, + Value: "2001:db8::/32", + }, + }, + Labels: nil, + }, + want: RouteDetails{ + DestType: cidrv6, + DestValue: "2001:db8::/32", + }, + }, + { + description: "CIDRv4 destination, IPv4 nexthop, with labels", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: cidrv4, + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: ipv4, + Value: "10.0.0.1", + }, + }, + Labels: map[string]interface{}{ + "key": "value", + }, + }, + want: RouteDetails{ + DestType: cidrv4, + DestValue: "10.0.0.0/24", + HopType: ipv4, + HopValue: "10.0.0.1", + Labels: "key: value", + }, + }, + { + description: "CIDRv6 destination, IPv6 nexthop, with no labels", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ + Type: cidrv6, + Value: "2001:db8::/32", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv6: &iaas.NexthopIPv6{ + Type: ipv6, + Value: "2001:db8::1", + }, + }, + Labels: nil, + }, + want: RouteDetails{ + DestType: cidrv6, + DestValue: "2001:db8::/32", + HopType: ipv6, + HopValue: "2001:db8::1", + }, + }, + { + description: "Internet nexthop without value", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: cidrv4, + Value: "0.0.0.0/0", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopInternet: &iaas.NexthopInternet{ + Type: "Internet", + }, + }, + Labels: nil, + }, + want: RouteDetails{ + DestType: cidrv4, + DestValue: "0.0.0.0/0", + HopType: "Internet", + // HopValue empty + }, + }, + { + description: "Blackhole nexthop without value and nil labels map", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv6: &iaas.DestinationCIDRv6{ + Type: cidrv6, + Value: "::/0", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopBlackhole: &iaas.NexthopBlackhole{ + Type: "Blackhole", + }, + }, + Labels: nil, + }, + want: RouteDetails{ + DestType: cidrv6, + DestValue: "::/0", + HopType: "Blackhole", + }, + }, + { + description: "route with created and updated timestamps", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: cidrv4, + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: ipv4, + Value: "10.0.0.1", + }, + }, + CreatedAt: &created, + UpdatedAt: &updated, + }, + want: RouteDetails{ + DestType: cidrv4, + DestValue: "10.0.0.0/24", + HopType: ipv4, + HopValue: "10.0.0.1", + CreatedAt: created.Format(time.RFC3339), + UpdatedAt: updated.Format(time.RFC3339), + Labels: "", + }, + }, + { + description: "route with created timestamp only", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: cidrv4, + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: ipv4, + Value: "10.0.0.1", + }, + }, + CreatedAt: &created, + }, + want: RouteDetails{ + DestType: cidrv4, + DestValue: "10.0.0.0/24", + HopType: ipv4, + HopValue: "10.0.0.1", + CreatedAt: created.Format(time.RFC3339), + UpdatedAt: "", + Labels: "", + }, + }, + { + description: "route with updated timestamp only", + input: &iaas.Route{ + Destination: iaas.RouteDestination{ + DestinationCIDRv4: &iaas.DestinationCIDRv4{ + Type: cidrv4, + Value: "10.0.0.0/24", + }, + }, + Nexthop: iaas.RouteNexthop{ + NexthopIPv4: &iaas.NexthopIPv4{ + Type: ipv4, + Value: "10.0.0.1", + }, + }, + UpdatedAt: &updated, + }, + want: RouteDetails{ + DestType: cidrv4, + DestValue: "10.0.0.0/24", + HopType: ipv4, + HopValue: "10.0.0.1", + CreatedAt: "", + UpdatedAt: updated.Format(time.RFC3339), + Labels: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + got := ExtractRouteDetails(tt.input) + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Fatalf("ExtractRouteDetails() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/pkg/services/object-storage/client/client.go b/internal/pkg/services/object-storage/client/client.go index 5eab52238..81e5dbe95 100644 --- a/internal/pkg/services/object-storage/client/client.go +++ b/internal/pkg/services/object-storage/client/client.go @@ -1,48 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*objectstorage.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ObjectStorageCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := objectstorage.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ObjectStorageCustomEndpointKey), false, genericclient.CreateApiClient[*objectstorage.APIClient](objectstorage.NewAPIClient)) } diff --git a/internal/pkg/services/object-storage/utils/utils.go b/internal/pkg/services/object-storage/utils/utils.go index bd23d0854..a122cb1ef 100644 --- a/internal/pkg/services/object-storage/utils/utils.go +++ b/internal/pkg/services/object-storage/utils/utils.go @@ -6,17 +6,11 @@ import ( "net/http" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" ) -type ObjectStorageClient interface { - GetServiceStatusExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error) - ListCredentialsGroupsExecute(ctx context.Context, projectId, region string) (*objectstorage.ListCredentialsGroupsResponse, error) - ListAccessKeys(ctx context.Context, projectId, region string) objectstorage.ApiListAccessKeysRequest -} - -func ProjectEnabled(ctx context.Context, apiClient ObjectStorageClient, projectId, region string) (bool, error) { - _, err := apiClient.GetServiceStatusExecute(ctx, projectId, region) +func ProjectEnabled(ctx context.Context, apiClient objectstorage.DefaultAPI, projectId, region string) (bool, error) { + _, err := apiClient.GetServiceStatus(ctx, projectId, region).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if !ok { @@ -30,8 +24,8 @@ func ProjectEnabled(ctx context.Context, apiClient ObjectStorageClient, projectI return true, nil } -func GetCredentialsGroupName(ctx context.Context, apiClient ObjectStorageClient, projectId, credentialsGroupId, region string) (string, error) { - resp, err := apiClient.ListCredentialsGroupsExecute(ctx, projectId, region) +func GetCredentialsGroupName(ctx context.Context, apiClient objectstorage.DefaultAPI, projectId, credentialsGroupId, region string) (string, error) { + resp, err := apiClient.ListCredentialsGroups(ctx, projectId, region).Execute() if err != nil { return "", fmt.Errorf("list Object Storage credentials groups: %w", err) } @@ -41,16 +35,16 @@ func GetCredentialsGroupName(ctx context.Context, apiClient ObjectStorageClient, return "", fmt.Errorf("nil Object Storage credentials group list: %w", err) } - for _, group := range *credentialsGroups { - if group.CredentialsGroupId != nil && *group.CredentialsGroupId == credentialsGroupId && group.DisplayName != nil && *group.DisplayName != "" { - return *group.DisplayName, nil + for _, group := range credentialsGroups { + if group.CredentialsGroupId == credentialsGroupId && group.DisplayName != "" { + return group.DisplayName, nil } } return "", fmt.Errorf("could not find Object Storage credentials group name") } -func GetCredentialsName(ctx context.Context, apiClient ObjectStorageClient, projectId, credentialsGroupId, keyId, region string) (string, error) { +func GetCredentialsName(ctx context.Context, apiClient objectstorage.DefaultAPI, projectId, credentialsGroupId, keyId, region string) (string, error) { req := apiClient.ListAccessKeys(ctx, projectId, region) req = req.CredentialsGroup(credentialsGroupId) resp, err := req.Execute() @@ -64,9 +58,9 @@ func GetCredentialsName(ctx context.Context, apiClient ObjectStorageClient, proj return "", fmt.Errorf("nil Object Storage credentials list") } - for _, credential := range *credentials { - if credential.KeyId != nil && *credential.KeyId == keyId && credential.DisplayName != nil && *credential.DisplayName != "" { - return *credential.DisplayName, nil + for _, credential := range credentials { + if credential.KeyId == keyId && credential.DisplayName != "" { + return credential.DisplayName, nil } } diff --git a/internal/pkg/services/object-storage/utils/utils_test.go b/internal/pkg/services/object-storage/utils/utils_test.go index 65b176172..1a93b9968 100644 --- a/internal/pkg/services/object-storage/utils/utils_test.go +++ b/internal/pkg/services/object-storage/utils/utils_test.go @@ -2,59 +2,69 @@ package utils import ( "context" - "encoding/json" "fmt" "net/http" - "net/http/httptest" "testing" "github.com/google/uuid" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + objectstorage "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) var ( testProjectId = uuid.NewString() testCredentialsGroupId = uuid.NewString() - testCredentialsId = "credentialsID" //nolint:gosec // linter false positive - testRegion = "eu01" ) const ( testCredentialsGroupName = "testGroup" testCredentialsName = "testCredential" + testCredentialsId = "credentialsID" //nolint:gosec // linter false positive + testRegion = "eu01" ) -type objectStorageClientMocked struct { - serviceDisabled bool - getServiceStatusFails bool +type mockSettings struct { + serviceDisabled bool + getServiceStatusFails bool + listCredentialsGroupsFails bool listCredentialsGroupsResp *objectstorage.ListCredentialsGroupsResponse - listAccessKeysReq objectstorage.ApiListAccessKeysRequest -} -func (m *objectStorageClientMocked) GetServiceStatusExecute(_ context.Context, _, _ string) (*objectstorage.ProjectStatus, error) { - if m.getServiceStatusFails { - return nil, fmt.Errorf("could not get service status") - } - if m.serviceDisabled { - return nil, &oapierror.GenericOpenAPIError{StatusCode: 404} - } - return &objectstorage.ProjectStatus{}, nil + listAccessKeysFails bool + listAccessKeysResp *objectstorage.ListAccessKeysResponse } -func (m *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Context, _, _ string) (*objectstorage.ListCredentialsGroupsResponse, error) { - if m.listCredentialsGroupsFails { - return nil, fmt.Errorf("could not list credentials groups") - } - return m.listCredentialsGroupsResp, nil -} +func newAPIMock(settings *mockSettings) objectstorage.DefaultAPI { + return &objectstorage.DefaultAPIServiceMock{ + GetServiceStatusExecuteMock: utils.Ptr(func(_ objectstorage.ApiGetServiceStatusRequest) (*objectstorage.ProjectStatus, error) { + if settings.getServiceStatusFails { + return nil, fmt.Errorf("could not get service status") + } -func (m *objectStorageClientMocked) ListAccessKeys(_ context.Context, _, _ string) objectstorage.ApiListAccessKeysRequest { - return m.listAccessKeysReq + if settings.serviceDisabled { + return nil, &oapierror.GenericOpenAPIError{StatusCode: http.StatusNotFound} + } + + return &objectstorage.ProjectStatus{}, nil + }), + ListCredentialsGroupsExecuteMock: utils.Ptr(func(_ objectstorage.ApiListCredentialsGroupsRequest) (*objectstorage.ListCredentialsGroupsResponse, error) { + if settings.listCredentialsGroupsFails { + return nil, fmt.Errorf("could not list credentials groups") + } + + return settings.listCredentialsGroupsResp, nil + }), + ListAccessKeysExecuteMock: utils.Ptr(func(_ objectstorage.ApiListAccessKeysRequest) (*objectstorage.ListAccessKeysResponse, error) { + if settings.listAccessKeysFails { + return nil, &oapierror.GenericOpenAPIError{StatusCode: http.StatusBadGateway} + } + + return settings.listAccessKeysResp, nil + }), + } } func TestProjectEnabled(t *testing.T) { @@ -85,10 +95,10 @@ func TestProjectEnabled(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &objectStorageClientMocked{ + client := newAPIMock(&mockSettings{ serviceDisabled: tt.serviceDisabled, getServiceStatusFails: tt.getProjectFails, - } + }) output, err := ProjectEnabled(context.Background(), client, testProjectId, testRegion) @@ -120,10 +130,10 @@ func TestGetCredentialsGroupName(t *testing.T) { { description: "base", listCredentialsGroupsResp: &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ + CredentialsGroups: []objectstorage.CredentialsGroup{ { - CredentialsGroupId: utils.Ptr(testCredentialsGroupId), - DisplayName: utils.Ptr(testCredentialsGroupName), + CredentialsGroupId: testCredentialsGroupId, + DisplayName: testCredentialsGroupName, }, }, }, @@ -138,14 +148,14 @@ func TestGetCredentialsGroupName(t *testing.T) { { description: "multiple credentials groups", listCredentialsGroupsResp: &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ + CredentialsGroups: []objectstorage.CredentialsGroup{ { - CredentialsGroupId: utils.Ptr("test-UUID"), - DisplayName: utils.Ptr("test-name"), + CredentialsGroupId: "test-UUID", + DisplayName: "test-name", }, { - CredentialsGroupId: utils.Ptr(testCredentialsGroupId), - DisplayName: utils.Ptr(testCredentialsGroupName), + CredentialsGroupId: testCredentialsGroupId, + DisplayName: testCredentialsGroupName, }, }, }, @@ -160,23 +170,11 @@ func TestGetCredentialsGroupName(t *testing.T) { isValid: false, }, { - description: "nil credentials group id", + description: "empty credentials group id", listCredentialsGroupsResp: &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ + CredentialsGroups: []objectstorage.CredentialsGroup{ { - CredentialsGroupId: nil, - }, - }, - }, - isValid: false, - }, - { - description: "nil credentials group name", - listCredentialsGroupsResp: &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ - { - CredentialsGroupId: utils.Ptr(testCredentialsGroupId), - DisplayName: nil, + CredentialsGroupId: "", }, }, }, @@ -185,10 +183,10 @@ func TestGetCredentialsGroupName(t *testing.T) { { description: "empty credentials group name", listCredentialsGroupsResp: &objectstorage.ListCredentialsGroupsResponse{ - CredentialsGroups: &[]objectstorage.CredentialsGroup{ + CredentialsGroups: []objectstorage.CredentialsGroup{ { - CredentialsGroupId: utils.Ptr(testCredentialsGroupId), - DisplayName: utils.Ptr(""), + CredentialsGroupId: testCredentialsGroupId, + DisplayName: "", }, }, }, @@ -198,10 +196,10 @@ func TestGetCredentialsGroupName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &objectStorageClientMocked{ + client := newAPIMock(&mockSettings{ listCredentialsGroupsFails: tt.listCredentialsGroupsFails, listCredentialsGroupsResp: tt.listCredentialsGroupsResp, - } + }) output, err := GetCredentialsGroupName(context.Background(), client, testProjectId, testCredentialsGroupId, testRegion) @@ -232,10 +230,10 @@ func TestGetCredentialsName(t *testing.T) { { description: "base", listAccessKeysResp: &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ + AccessKeys: []objectstorage.AccessKey{ { - KeyId: utils.Ptr(testCredentialsId), - DisplayName: utils.Ptr(testCredentialsName), + KeyId: testCredentialsId, + DisplayName: testCredentialsName, }, }, }, @@ -250,14 +248,14 @@ func TestGetCredentialsName(t *testing.T) { { description: "multiple credentials", listAccessKeysResp: &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ + AccessKeys: []objectstorage.AccessKey{ { - KeyId: utils.Ptr("test-UUID"), - DisplayName: utils.Ptr("test-name"), + KeyId: "test-UUID", + DisplayName: "test-name", }, { - KeyId: utils.Ptr(testCredentialsId), - DisplayName: utils.Ptr(testCredentialsName), + KeyId: testCredentialsId, + DisplayName: testCredentialsName, }, }, }, @@ -272,23 +270,11 @@ func TestGetCredentialsName(t *testing.T) { isValid: false, }, { - description: "nil credentials id", - listAccessKeysResp: &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ - { - KeyId: nil, - }, - }, - }, - isValid: false, - }, - { - description: "nil credentials name", + description: "empty credentials id", listAccessKeysResp: &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ + AccessKeys: []objectstorage.AccessKey{ { - KeyId: utils.Ptr(testCredentialsId), - DisplayName: nil, + KeyId: "", }, }, }, @@ -297,10 +283,10 @@ func TestGetCredentialsName(t *testing.T) { { description: "empty credentials name", listAccessKeysResp: &objectstorage.ListAccessKeysResponse{ - AccessKeys: &[]objectstorage.AccessKey{ + AccessKeys: []objectstorage.AccessKey{ { - KeyId: utils.Ptr(testCredentialsId), - DisplayName: utils.Ptr(""), + KeyId: testCredentialsId, + DisplayName: "", }, }, }, @@ -310,37 +296,10 @@ func TestGetCredentialsName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - mockedRespBytes, err := json.Marshal(tt.listAccessKeysResp) - if err != nil { - t.Fatalf("Failed to marshal mocked response: %v", err) - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - if tt.getCredentialsNameFails { - w.WriteHeader(http.StatusBadGateway) - w.Header().Set("Content-Type", "application/json") - _, err := w.Write([]byte("{\"message\": \"Something bad happened\"")) - if err != nil { - t.Errorf("Failed to write bad response: %v", err) - } - return - } - - _, err := w.Write(mockedRespBytes) - if err != nil { - t.Errorf("Failed to write response: %v", err) - } + client := newAPIMock(&mockSettings{ + listAccessKeysFails: tt.getCredentialsNameFails, + listAccessKeysResp: tt.listAccessKeysResp, }) - mockedServer := httptest.NewServer(handler) - defer mockedServer.Close() - client, err := objectstorage.NewAPIClient( - config.WithEndpoint(mockedServer.URL), - config.WithoutAuthentication(), - ) - if err != nil { - t.Fatalf("Failed to initialize client: %v", err) - } output, err := GetCredentialsName(context.Background(), client, testProjectId, testCredentialsGroupId, testCredentialsId, testRegion) diff --git a/internal/pkg/services/observability/client/client.go b/internal/pkg/services/observability/client/client.go index 23698b5ff..b8e5a5fbc 100644 --- a/internal/pkg/services/observability/client/client.go +++ b/internal/pkg/services/observability/client/client.go @@ -1,47 +1,15 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" ) func ConfigureClient(p *print.Printer, cliVersion string) (*observability.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ObservabilityCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := observability.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ObservabilityCustomEndpointKey), true, genericclient.CreateApiClient[*observability.APIClient](observability.NewAPIClient)) } diff --git a/internal/pkg/services/observability/utils/utils.go b/internal/pkg/services/observability/utils/utils.go index 234da09be..c5b2be317 100644 --- a/internal/pkg/services/observability/utils/utils.go +++ b/internal/pkg/services/observability/utils/utils.go @@ -3,38 +3,34 @@ package utils import ( "context" "fmt" + "reflect" "strings" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/observability" ) const ( service = "observability" ) -type ObservabilityClient interface { - GetInstanceExecute(ctx context.Context, instanceId, projectId string) (*observability.GetInstanceResponse, error) - GetGrafanaConfigsExecute(ctx context.Context, instanceId, projectId string) (*observability.GrafanaConfigs, error) - UpdateGrafanaConfigs(ctx context.Context, instanceId string, projectId string) observability.ApiUpdateGrafanaConfigsRequest -} - var ( defaultStaticConfigs = []observability.CreateScrapeConfigPayloadStaticConfigsInner{ { - Targets: utils.Ptr([]string{ + Targets: []string{ "url-target", - }), + }, }, } DefaultCreateScrapeConfigPayload = observability.CreateScrapeConfigPayload{ - JobName: utils.Ptr("default-name"), + JobName: "default-name", MetricsPath: utils.Ptr("/metrics"), - Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTPS.Ptr(), - ScrapeInterval: utils.Ptr("5m"), - ScrapeTimeout: utils.Ptr("2m"), - StaticConfigs: utils.Ptr(defaultStaticConfigs), + Scheme: observability.CREATESCRAPECONFIGPAYLOADSCHEME_HTTPS, + ScrapeInterval: "5m", + ScrapeTimeout: "2m", + StaticConfigs: defaultStaticConfigs, } ) @@ -43,9 +39,9 @@ func ValidatePlanId(planId string, resp *observability.PlansResponse) error { return fmt.Errorf("no Observability plans provided") } - for i := range *resp.Plans { - plan := (*resp.Plans)[i] - if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + for i := range resp.Plans { + plan := resp.Plans[i] + if strings.EqualFold(plan.Id, planId) { return nil } } @@ -56,32 +52,32 @@ func ValidatePlanId(planId string, resp *observability.PlansResponse) error { } } -func LoadPlanId(planName string, resp *observability.PlansResponse) (*string, error) { +func LoadPlanId(planName string, resp *observability.PlansResponse) (string, error) { availablePlanNames := "" if resp == nil { - return nil, fmt.Errorf("no Observability plans provided") + return "", fmt.Errorf("no Observability plans provided") } - for i := range *resp.Plans { - plan := (*resp.Plans)[i] + for i := range resp.Plans { + plan := resp.Plans[i] if plan.Name == nil { continue } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { + if strings.EqualFold(*plan.Name, planName) { return plan.Id, nil } availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } details := fmt.Sprintf("You provided plan name %q, which is invalid. Available plan names are: %s", planName, availablePlanNames) - return nil, &errors.ObservabilityInvalidPlanError{ + return "", &errors.ObservabilityInvalidPlanError{ Service: service, Details: details, } } func MapToUpdateScrapeConfigPayload(resp *observability.GetScrapeConfigResponse) (*observability.UpdateScrapeConfigPayload, error) { - if resp == nil || resp.Data == nil { + if resp == nil { return nil, fmt.Errorf("no Observability scrape config provided") } @@ -92,9 +88,14 @@ func MapToUpdateScrapeConfigPayload(resp *observability.GetScrapeConfigResponse) tlsConfig := mapTlsConfig(data.TlsConfig) metricsRelabelConfigs := mapMetricsRelabelConfig(data.MetricsRelabelConfigs) - var params *map[string]interface{} + var params map[string]interface{} if data.Params != nil { - params = utils.Ptr(mapParams(*data.Params)) + params = mapParams(*data.Params) + } + + var scheme observability.UpdateScrapeConfigPayloadScheme + if data.Scheme != nil { + scheme = observability.UpdateScrapeConfigPayloadScheme(*data.Scheme) } payload := observability.UpdateScrapeConfigPayload{ @@ -102,33 +103,33 @@ func MapToUpdateScrapeConfigPayload(resp *observability.GetScrapeConfigResponse) BearerToken: data.BearerToken, HonorLabels: data.HonorLabels, HonorTimeStamps: data.HonorTimeStamps, - MetricsPath: data.MetricsPath, + MetricsPath: utils.PtrString(data.MetricsPath), MetricsRelabelConfigs: metricsRelabelConfigs, Params: params, - SampleLimit: utils.ConvertInt64PToFloat64P(data.SampleLimit), - Scheme: observability.UpdateScrapeConfigPayloadGetSchemeAttributeType(data.Scheme), + SampleLimit: utils.ConvertInt32PToFloat32P(data.SampleLimit), + Scheme: scheme, ScrapeInterval: data.ScrapeInterval, ScrapeTimeout: data.ScrapeTimeout, StaticConfigs: staticConfigs, TlsConfig: tlsConfig, } - if payload == (observability.UpdateScrapeConfigPayload{}) { + if reflect.DeepEqual(payload, observability.UpdateScrapeConfigPayload{}) { return nil, fmt.Errorf("the provided Observability scrape config payload is empty") } return &payload, nil } -func mapMetricsRelabelConfig(metricsRelabelConfigs *[]observability.MetricsRelabelConfig) *[]observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner { +func mapMetricsRelabelConfig(metricsRelabelConfigs []observability.MetricsRelabelConfig) []observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner { if metricsRelabelConfigs == nil { return nil } - var mappedConfigs []observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner - for _, config := range *metricsRelabelConfigs { - mappedConfig := observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner{ - Action: observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInnerGetActionAttributeType(config.Action), - Modulus: utils.ConvertInt64PToFloat64P(config.Modulus), + var mappedConfigs []observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner + for _, config := range metricsRelabelConfigs { + mappedConfig := observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner{ + Action: (*observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInnerAction)(config.Action), + Modulus: utils.ConvertInt32PToFloat32P(config.Modulus), Regex: config.Regex, Replacement: config.Replacement, Separator: config.Separator, @@ -137,18 +138,18 @@ func mapMetricsRelabelConfig(metricsRelabelConfigs *[]observability.MetricsRelab } mappedConfigs = append(mappedConfigs, mappedConfig) } - return &mappedConfigs + return mappedConfigs } -func mapStaticConfig(staticConfigs *[]observability.StaticConfigs) *[]observability.UpdateScrapeConfigPayloadStaticConfigsInner { +func mapStaticConfig(staticConfigs []observability.StaticConfigs) []observability.UpdateScrapeConfigPayloadStaticConfigsInner { if staticConfigs == nil { return nil } var mappedConfigs []observability.UpdateScrapeConfigPayloadStaticConfigsInner - for _, config := range *staticConfigs { - var labels *map[string]interface{} + for _, config := range staticConfigs { + var labels map[string]interface{} if config.Labels != nil { - labels = utils.Ptr(mapStaticConfigLabels(*config.Labels)) + labels = mapStaticConfigLabels(*config.Labels) } mappedConfig := observability.UpdateScrapeConfigPayloadStaticConfigsInner{ Labels: labels, @@ -157,26 +158,34 @@ func mapStaticConfig(staticConfigs *[]observability.StaticConfigs) *[]observabil mappedConfigs = append(mappedConfigs, mappedConfig) } - return &mappedConfigs + return mappedConfigs } -func mapBasicAuth(basicAuth *observability.BasicAuth) *observability.CreateScrapeConfigPayloadBasicAuth { +func mapBasicAuth(basicAuth *observability.BasicAuth) *observability.UpdateScrapeConfigPayloadBasicAuth { if basicAuth == nil { return nil } - return &observability.CreateScrapeConfigPayloadBasicAuth{ - Password: basicAuth.Password, - Username: basicAuth.Username, + var password, username *string + if basicAuth.Password != "" { + password = &basicAuth.Password + } + if basicAuth.Username != "" { + username = &basicAuth.Username + } + + return &observability.UpdateScrapeConfigPayloadBasicAuth{ + Password: password, + Username: username, } } -func mapTlsConfig(tlsConfig *observability.TLSConfig) *observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig { +func mapTlsConfig(tlsConfig *observability.TLSConfig) *observability.UpdateScrapeConfigPayloadTlsConfig { if tlsConfig == nil { return nil } - return &observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig{ + return &observability.UpdateScrapeConfigPayloadTlsConfig{ InsecureSkipVerify: tlsConfig.InsecureSkipVerify, } } @@ -197,8 +206,8 @@ func mapStaticConfigLabels(labels map[string]string) map[string]interface{} { return labelsMap } -func GetInstanceName(ctx context.Context, apiClient ObservabilityClient, instanceId, projectId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, instanceId, projectId) +func GetInstanceName(ctx context.Context, apiClient observability.DefaultAPI, instanceId, projectId string) (string, error) { + resp, err := apiClient.GetInstance(ctx, instanceId, projectId).Execute() if err != nil { return "", fmt.Errorf("get Observability instance: %w", err) } @@ -224,8 +233,8 @@ func ToPayloadGenericOAuth(respOAuth *observability.GrafanaOauth) *observability } } -func GetPartialUpdateGrafanaConfigsPayload(ctx context.Context, apiClient ObservabilityClient, instanceId, projectId string, singleSignOn, publicReadAccess *bool) (*observability.UpdateGrafanaConfigsPayload, error) { - currentConfigs, err := apiClient.GetGrafanaConfigsExecute(ctx, instanceId, projectId) +func GetPartialUpdateGrafanaConfigsPayload(ctx context.Context, apiClient observability.DefaultAPI, instanceId, projectId string, singleSignOn, publicReadAccess *bool) (*observability.UpdateGrafanaConfigsPayload, error) { + currentConfigs, err := apiClient.GetGrafanaConfigs(ctx, instanceId, projectId).Execute() if err != nil { return nil, fmt.Errorf("get current Grafana configs: %w", err) } diff --git a/internal/pkg/services/observability/utils/utils_test.go b/internal/pkg/services/observability/utils/utils_test.go index 0b5083aad..12ffaca07 100644 --- a/internal/pkg/services/observability/utils/utils_test.go +++ b/internal/pkg/services/observability/utils/utils_test.go @@ -10,11 +10,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/observability" + observability "github.com/stackitcloud/stackit-sdk-go/services/observability/v1api" ) var ( - testClient = &observability.APIClient{} testProjectId = uuid.NewString() testInstanceId = uuid.NewString() testPlanId = uuid.NewString() @@ -26,34 +25,34 @@ const ( ) var testPlansResponse = observability.PlansResponse{ - Plans: &[]observability.Plan{ + Plans: []observability.Plan{ { - Id: utils.Ptr(testPlanId), + Id: testPlanId, Name: utils.Ptr(testPlanName), }, }, } func fixtureGetScrapeConfigResponse(mods ...func(*observability.GetScrapeConfigResponse)) *observability.GetScrapeConfigResponse { - number := int64(1) + number := int32(1) resp := &observability.GetScrapeConfigResponse{ - Data: &observability.Job{ + Data: observability.Job{ BasicAuth: &observability.BasicAuth{ - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), + Username: "username", + Password: "password", }, BearerToken: utils.Ptr("bearerToken"), HonorLabels: utils.Ptr(true), HonorTimeStamps: utils.Ptr(true), MetricsPath: utils.Ptr("/metrics"), - MetricsRelabelConfigs: &[]observability.MetricsRelabelConfig{ + MetricsRelabelConfigs: []observability.MetricsRelabelConfig{ { - Action: observability.METRICSRELABELCONFIGACTION_REPLACE.Ptr(), + Action: observability.ACTION_REPLACE.Ptr(), Modulus: &number, Regex: utils.Ptr("regex"), Replacement: utils.Ptr("replacement"), Separator: utils.Ptr("separator"), - SourceLabels: &[]string{"sourceLabel"}, + SourceLabels: []string{"sourceLabel"}, TargetLabel: utils.Ptr("targetLabel"), }, }, @@ -62,16 +61,16 @@ func fixtureGetScrapeConfigResponse(mods ...func(*observability.GetScrapeConfigR "key2": {}, }, SampleLimit: &number, - Scheme: observability.JOBSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("interval"), - ScrapeTimeout: utils.Ptr("timeout"), - StaticConfigs: &[]observability.StaticConfigs{ + Scheme: observability.SCHEME_HTTP.Ptr(), + ScrapeInterval: "interval", + ScrapeTimeout: "timeout", + StaticConfigs: []observability.StaticConfigs{ { Labels: &map[string]string{ "label": "value", "label2": "value2", }, - Targets: &[]string{"target"}, + Targets: []string{"target"}, }, }, TlsConfig: &observability.TLSConfig{ @@ -89,43 +88,43 @@ func fixtureGetScrapeConfigResponse(mods ...func(*observability.GetScrapeConfigR func fixtureUpdateScrapeConfigPayload(mods ...func(*observability.UpdateScrapeConfigPayload)) *observability.UpdateScrapeConfigPayload { payload := &observability.UpdateScrapeConfigPayload{ - BasicAuth: &observability.CreateScrapeConfigPayloadBasicAuth{ + BasicAuth: &observability.UpdateScrapeConfigPayloadBasicAuth{ Username: utils.Ptr("username"), Password: utils.Ptr("password"), }, BearerToken: utils.Ptr("bearerToken"), HonorLabels: utils.Ptr(true), HonorTimeStamps: utils.Ptr(true), - MetricsPath: utils.Ptr("/metrics"), - MetricsRelabelConfigs: &[]observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner{ + MetricsPath: "/metrics", + MetricsRelabelConfigs: []observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner{ { - Action: utils.Ptr(observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInnerAction("replace")), - Modulus: utils.Ptr(1.0), + Action: observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInnerAction("replace").Ptr(), + Modulus: utils.Ptr(float32(1.0)), Regex: utils.Ptr("regex"), Replacement: utils.Ptr("replacement"), Separator: utils.Ptr("separator"), - SourceLabels: &[]string{"sourceLabel"}, + SourceLabels: []string{"sourceLabel"}, TargetLabel: utils.Ptr("targetLabel"), }, }, - Params: &map[string]interface{}{ + Params: map[string]interface{}{ "key": []string{"value1", "value2"}, "key2": []string{}, }, - SampleLimit: utils.Ptr(1.0), - Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP.Ptr(), - ScrapeInterval: utils.Ptr("interval"), - ScrapeTimeout: utils.Ptr("timeout"), - StaticConfigs: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{ + SampleLimit: utils.Ptr(float32(1.0)), + Scheme: observability.UPDATESCRAPECONFIGPAYLOADSCHEME_HTTP, + ScrapeInterval: "interval", + ScrapeTimeout: "timeout", + StaticConfigs: []observability.UpdateScrapeConfigPayloadStaticConfigsInner{ { - Labels: &map[string]interface{}{ + Labels: map[string]interface{}{ "label": "value", "label2": "value2", }, - Targets: &[]string{"target"}, + Targets: []string{"target"}, }, }, - TlsConfig: &observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig{ + TlsConfig: &observability.UpdateScrapeConfigPayloadTlsConfig{ InsecureSkipVerify: utils.Ptr(true), }, } @@ -144,37 +143,36 @@ type observabilityClientMocked struct { getGrafanaConfigsResp *observability.GrafanaConfigs } -func (m *observabilityClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*observability.GetInstanceResponse, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *observabilityClientMocked) GetGrafanaConfigsExecute(_ context.Context, _, _ string) (*observability.GrafanaConfigs, error) { - if m.getGrafanaConfigsFails { - return nil, fmt.Errorf("could not get grafana configs") +func (m *observabilityClientMocked) newMock() observability.DefaultAPI { + return observability.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ observability.ApiGetInstanceRequest) (*observability.GetInstanceResponse, error) { + if m.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return m.getInstanceResp, nil + }), + GetGrafanaConfigsExecuteMock: utils.Ptr(func(_ observability.ApiGetGrafanaConfigsRequest) (*observability.GrafanaConfigs, error) { + if m.getGrafanaConfigsFails { + return nil, fmt.Errorf("could not get grafana configs") + } + return m.getGrafanaConfigsResp, nil + }), } - return m.getGrafanaConfigsResp, nil -} - -func (c *observabilityClientMocked) UpdateGrafanaConfigs(ctx context.Context, instanceId, projectId string) observability.ApiUpdateGrafanaConfigsRequest { - return testClient.UpdateGrafanaConfigs(ctx, instanceId, projectId) } func fixtureGrafanaConfigs(mods ...func(gc *observability.GrafanaConfigs)) *observability.GrafanaConfigs { gc := observability.GrafanaConfigs{ - GenericOauth: &observability.GrafanaOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + GenericOauth: &observability.GrafanaOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, PublicReadAccess: utils.Ptr(false), @@ -216,7 +214,7 @@ func TestGetInstanceName(t *testing.T) { getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testInstanceId, testProjectId) + output, err := GetInstanceName(context.Background(), client.newMock(), testInstanceId, testProjectId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -276,7 +274,7 @@ func TestLoadPlanId(t *testing.T) { description: "no available plans", planName: testPlanName, plansResponse: &observability.PlansResponse{ - Plans: &[]observability.Plan{}, + Plans: []observability.Plan{}, }, isValid: false, }, @@ -295,8 +293,8 @@ func TestLoadPlanId(t *testing.T) { if !tt.isValid { return } - if *output != tt.expectedOutput { - t.Errorf("expected output to be %s, got %s", tt.expectedOutput, *output) + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) } }) } @@ -341,7 +339,7 @@ func TestValidatePlanId(t *testing.T) { description: "no available plans", planId: testPlanId, plansResponse: &observability.PlansResponse{ - Plans: &[]observability.Plan{}, + Plans: []observability.Plan{}, }, isValid: false, }, @@ -382,17 +380,10 @@ func TestMapToUpdateScrapeConfigPayload(t *testing.T) { resp: nil, isValid: false, }, - { - description: "nil data", - resp: &observability.GetScrapeConfigResponse{ - Data: nil, - }, - isValid: false, - }, { description: "empty data", resp: &observability.GetScrapeConfigResponse{ - Data: &observability.Job{}, + Data: observability.Job{}, }, isValid: false, }, @@ -425,37 +416,37 @@ func TestMapToUpdateScrapeConfigPayload(t *testing.T) { func TestMapMetricsRelabelConfig(t *testing.T) { tests := []struct { description string - config *[]observability.MetricsRelabelConfig - expected *[]observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner + config []observability.MetricsRelabelConfig + expected *[]observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner }{ { description: "base case", - config: &[]observability.MetricsRelabelConfig{ + config: []observability.MetricsRelabelConfig{ { - Action: observability.METRICSRELABELCONFIGACTION_REPLACE.Ptr(), - Modulus: utils.Int64Ptr(1), + Action: observability.ACTION_REPLACE.Ptr(), + Modulus: utils.Ptr(int32(1)), Regex: utils.Ptr("regex"), Replacement: utils.Ptr("replacement"), Separator: utils.Ptr("separator"), - SourceLabels: utils.Ptr([]string{"sourceLabel", "sourceLabel2"}), + SourceLabels: []string{"sourceLabel", "sourceLabel2"}, TargetLabel: utils.Ptr("targetLabel"), }, }, - expected: &[]observability.CreateScrapeConfigPayloadMetricsRelabelConfigsInner{ + expected: &[]observability.UpdateScrapeConfigPayloadMetricsRelabelConfigsInner{ { - Action: observability.CREATESCRAPECONFIGPAYLOADMETRICSRELABELCONFIGSINNERACTION_REPLACE.Ptr(), - Modulus: utils.Float64Ptr(1.0), + Action: observability.UPDATESCRAPECONFIGPAYLOADMETRICSRELABELCONFIGSINNERACTION_REPLACE.Ptr(), + Modulus: utils.Ptr(float32(1.0)), Regex: utils.Ptr("regex"), Replacement: utils.Ptr("replacement"), Separator: utils.Ptr("separator"), - SourceLabels: utils.Ptr([]string{"sourceLabel", "sourceLabel2"}), + SourceLabels: []string{"sourceLabel", "sourceLabel2"}, TargetLabel: utils.Ptr("targetLabel"), }, }, }, { description: "empty data", - config: &[]observability.MetricsRelabelConfig{}, + config: []observability.MetricsRelabelConfig{}, expected: nil, }, { @@ -469,11 +460,11 @@ func TestMapMetricsRelabelConfig(t *testing.T) { t.Run(tt.description, func(t *testing.T) { output := mapMetricsRelabelConfig(tt.config) - if tt.expected == nil && output == nil || *output == nil { + if tt.expected == nil && output == nil { return } - diff := cmp.Diff(*output, *tt.expected) + diff := cmp.Diff(output, *tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -484,33 +475,33 @@ func TestMapMetricsRelabelConfig(t *testing.T) { func TestMapStaticConfig(t *testing.T) { tests := []struct { description string - config *[]observability.StaticConfigs + config []observability.StaticConfigs expected *[]observability.UpdateScrapeConfigPayloadStaticConfigsInner }{ { description: "base case", - config: &[]observability.StaticConfigs{ + config: []observability.StaticConfigs{ { Labels: &map[string]string{ "label": "value", "label2": "value2", }, - Targets: &[]string{"target", "target2"}, + Targets: []string{"target", "target2"}, }, }, expected: &[]observability.UpdateScrapeConfigPayloadStaticConfigsInner{ { - Labels: utils.Ptr(map[string]interface{}{ + Labels: map[string]interface{}{ "label": "value", "label2": "value2", - }), - Targets: utils.Ptr([]string{"target", "target2"}), + }, + Targets: []string{"target", "target2"}, }, }, }, { description: "empty data", - config: &[]observability.StaticConfigs{}, + config: []observability.StaticConfigs{}, expected: nil, }, { @@ -524,11 +515,11 @@ func TestMapStaticConfig(t *testing.T) { t.Run(tt.description, func(t *testing.T) { output := mapStaticConfig(tt.config) - if tt.expected == nil && (output == nil || *output == nil) { + if tt.expected == nil && output == nil { return } - diff := cmp.Diff(*output, *tt.expected) + diff := cmp.Diff(output, *tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) } @@ -540,15 +531,15 @@ func TestMapBasicAuth(t *testing.T) { tests := []struct { description string auth *observability.BasicAuth - expected *observability.CreateScrapeConfigPayloadBasicAuth + expected *observability.UpdateScrapeConfigPayloadBasicAuth }{ { description: "base case", auth: &observability.BasicAuth{ - Username: utils.Ptr("username"), - Password: utils.Ptr("password"), + Username: "username", + Password: "password", }, - expected: &observability.CreateScrapeConfigPayloadBasicAuth{ + expected: &observability.UpdateScrapeConfigPayloadBasicAuth{ Username: utils.Ptr("username"), Password: utils.Ptr("password"), }, @@ -556,7 +547,7 @@ func TestMapBasicAuth(t *testing.T) { { description: "empty data", auth: &observability.BasicAuth{}, - expected: &observability.CreateScrapeConfigPayloadBasicAuth{}, + expected: &observability.UpdateScrapeConfigPayloadBasicAuth{}, }, { description: "nil", @@ -585,21 +576,21 @@ func TestMapTlsConfig(t *testing.T) { tests := []struct { description string config *observability.TLSConfig - expected *observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig + expected *observability.UpdateScrapeConfigPayloadTlsConfig }{ { description: "base case", config: &observability.TLSConfig{ InsecureSkipVerify: utils.Ptr(true), }, - expected: &observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig{ + expected: &observability.UpdateScrapeConfigPayloadTlsConfig{ InsecureSkipVerify: utils.Ptr(true), }, }, { description: "empty data", config: &observability.TLSConfig{}, - expected: &observability.CreateScrapeConfigPayloadHttpSdConfigsInnerOauth2TlsConfig{}, + expected: &observability.UpdateScrapeConfigPayloadTlsConfig{}, }, { description: "nil", @@ -708,30 +699,30 @@ func TestToPayloadGenericOAuth(t *testing.T) { }{ { description: "base", - response: &observability.GrafanaOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + response: &observability.GrafanaOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, - expected: &observability.UpdateGrafanaConfigsPayloadGenericOauth{ - ApiUrl: utils.Ptr("apiUrl"), - AuthUrl: utils.Ptr("authUrl"), - Enabled: utils.Ptr(true), + expected: &observability.UpdateGrafanaConfigsPayloadGenericOauth{ // nolint:gosec // false positive + ApiUrl: "apiUrl", + AuthUrl: "authUrl", + Enabled: true, Name: utils.Ptr("name"), - OauthClientId: utils.Ptr("oauthClientId"), - OauthClientSecret: utils.Ptr("oauthClientSecret"), - RoleAttributePath: utils.Ptr("roleAttributePath"), + OauthClientId: "oauthClientId", + OauthClientSecret: "oauthClientSecret", + RoleAttributePath: "roleAttributePath", RoleAttributeStrict: utils.Ptr(true), Scopes: utils.Ptr("scopes"), - TokenUrl: utils.Ptr("tokenUrl"), + TokenUrl: "tokenUrl", UsePkce: utils.Ptr(true), }, }, @@ -875,7 +866,7 @@ func TestGetPartialUpdateGrafanaConfigsPayload(t *testing.T) { getGrafanaConfigsResp: tt.getGrafanaConfigsResp, } - payload, err := GetPartialUpdateGrafanaConfigsPayload(context.Background(), client, testInstanceId, testProjectId, tt.singleSignOn, tt.publicReadAccess) + payload, err := GetPartialUpdateGrafanaConfigsPayload(context.Background(), client.newMock(), testInstanceId, testProjectId, tt.singleSignOn, tt.publicReadAccess) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -894,3 +885,36 @@ func TestGetPartialUpdateGrafanaConfigsPayload(t *testing.T) { }) } } + +// in internal/pkg/services/observability/utils/utils.go#mapMetricsRelabelConfig we cast from one enum to another, esure that both have the same values +func TestUpdateScrapeConfigPayloadMetricsRelabelConfigsInnerActionIsAction(t *testing.T) { + innerKeys := make(map[string]struct{}) + actionKeys := make(map[string]struct{}) + for _, innerEnumVal := range observability.AllowedUpdateScrapeConfigPayloadMetricsRelabelConfigsInnerActionEnumValues { + innerKeys[string(innerEnumVal)] = struct{}{} + } + for _, actionEnumVal := range observability.AllowedActionEnumValues { + actionKeys[string(actionEnumVal)] = struct{}{} + } + onlyInInner := make([]string, 0) + onlyInAction := make([]string, 0) + for innerKey := range innerKeys { + if _, ok := actionKeys[innerKey]; !ok { + onlyInInner = append(onlyInInner, innerKey) + } + } + for actionKey := range actionKeys { + if _, ok := innerKeys[actionKey]; !ok { + onlyInAction = append(onlyInAction, actionKey) + } + } + if len(onlyInInner) > 0 { + t.Logf("the following keys are only in AllowedUpdateScrapeConfigPayloadMetricsRelabelConfigsInnerActionEnumValues: %v", onlyInInner) + } + if len(onlyInAction) > 0 { + t.Logf("the following keys are only in AllowedActionEnumValues: %v", onlyInAction) + } + if len(onlyInInner) > 0 || len(onlyInAction) > 0 { + t.Fail() + } +} diff --git a/internal/pkg/services/opensearch/client/client.go b/internal/pkg/services/opensearch/client/client.go index b1948a389..f151d34e6 100644 --- a/internal/pkg/services/opensearch/client/client.go +++ b/internal/pkg/services/opensearch/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*opensearch.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.OpenSearchCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := opensearch.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.OpenSearchCustomEndpointKey), false, genericclient.CreateApiClient[*opensearch.APIClient](opensearch.NewAPIClient)) } diff --git a/internal/pkg/services/opensearch/utils/utils.go b/internal/pkg/services/opensearch/utils/utils.go index 58219f52c..9bc51b92b 100644 --- a/internal/pkg/services/opensearch/utils/utils.go +++ b/internal/pkg/services/opensearch/utils/utils.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" ) const ( @@ -15,9 +15,9 @@ const ( ) func ValidatePlanId(planId string, offerings *opensearch.ListOfferingsResponse) error { - for _, offer := range *offerings.Offerings { - for _, plan := range *offer.Plans { - if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + for _, offer := range offerings.Offerings { + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Id, planId) { return nil } } @@ -29,59 +29,51 @@ func ValidatePlanId(planId string, offerings *opensearch.ListOfferingsResponse) } } -func LoadPlanId(planName, version string, offerings *opensearch.ListOfferingsResponse) (*string, error) { +func LoadPlanId(planName, version string, offerings *opensearch.ListOfferingsResponse) (string, error) { availableVersions := "" availablePlanNames := "" isValidVersion := false - for _, offer := range *offerings.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) + for _, offer := range offerings.Offerings { + if !strings.EqualFold(offer.Version, version) { + availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, offer.Version) continue } isValidVersion = true - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue - } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Name, planName) { return plan.Id, nil } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, plan.Name) } } if !isValidVersion { details := fmt.Sprintf("You provided version %q, which is invalid. Available versions are: %s", version, availableVersions) - return nil, &errors.DSAInvalidPlanError{ + return "", &errors.DSAInvalidPlanError{ Service: service, Details: details, } } details := fmt.Sprintf("You provided plan_name %q for version %s, which is invalid. Available plan names for that version are: %s", planName, version, availablePlanNames) - return nil, &errors.DSAInvalidPlanError{ + return "", &errors.DSAInvalidPlanError{ Service: service, Details: details, } } -type OpenSearchClient interface { - GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*opensearch.Instance, error) - GetCredentialsExecute(ctx context.Context, projectId, instanceId, credentialsId string) (*opensearch.CredentialsResponse, error) -} - -func GetInstanceName(ctx context.Context, apiClient OpenSearchClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) +func GetInstanceName(ctx context.Context, apiClient opensearch.DefaultAPI, projectId, region, instanceId string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { return "", fmt.Errorf("get OpenSearch instance: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetCredentialsUsername(ctx context.Context, apiClient OpenSearchClient, projectId, instanceId, credentialsId string) (string, error) { - resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) +func GetCredentialsUsername(ctx context.Context, apiClient opensearch.DefaultAPI, projectId, region, instanceId, credentialsId string) (string, error) { + resp, err := apiClient.GetCredentials(ctx, projectId, region, instanceId, credentialsId).Execute() if err != nil { return "", fmt.Errorf("get OpenSearch credentials: %w", err) } - return *resp.Raw.Credentials.Username, nil + return resp.Raw.Credentials.Username, nil } diff --git a/internal/pkg/services/opensearch/utils/utils_test.go b/internal/pkg/services/opensearch/utils/utils_test.go index 8ebe3391b..114e8230b 100644 --- a/internal/pkg/services/opensearch/utils/utils_test.go +++ b/internal/pkg/services/opensearch/utils/utils_test.go @@ -5,10 +5,10 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + opensearch "github.com/stackitcloud/stackit-sdk-go/services/opensearch/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) var ( @@ -18,62 +18,64 @@ var ( ) const ( + testRegion = "eu01" testInstanceName = "instance" testCredentialsUsername = "username" ) -type openSearchClientMocked struct { +type mockSettings struct { getInstanceFails bool getInstanceResp *opensearch.Instance getCredentialsFails bool getCredentialsResp *opensearch.CredentialsResponse } -func (m *openSearchClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*opensearch.Instance, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *openSearchClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*opensearch.CredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get user") +func newAPIClientMock(m mockSettings) opensearch.DefaultAPI { + return opensearch.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ opensearch.ApiGetInstanceRequest) (*opensearch.Instance, error) { + if m.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return m.getInstanceResp, nil + }), + GetCredentialsExecuteMock: utils.Ptr(func(_ opensearch.ApiGetCredentialsRequest) (*opensearch.CredentialsResponse, error) { + if m.getCredentialsFails { + return nil, fmt.Errorf("could not get user") + } + return m.getCredentialsResp, nil + }), } - return m.getCredentialsResp, nil } func TestGetInstanceName(t *testing.T) { tests := []struct { - description string - getInstanceFails bool - getInstanceResp *opensearch.Instance - isValid bool - expectedOutput string + description string + mockClientSettings mockSettings + isValid bool + expectedOutput string }{ { description: "base", - getInstanceResp: &opensearch.Instance{ - Name: utils.Ptr(testInstanceName), + mockClientSettings: mockSettings{ + getInstanceResp: &opensearch.Instance{ + Name: testInstanceName, + }, }, isValid: true, expectedOutput: testInstanceName, }, { - description: "get instance fails", - getInstanceFails: true, - isValid: false, + description: "get instance fails", + mockClientSettings: mockSettings{ + getInstanceFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &openSearchClientMocked{ - getInstanceFails: tt.getInstanceFails, - getInstanceResp: tt.getInstanceResp, - } - - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + output, err := GetInstanceName(context.Background(), newAPIClientMock(tt.mockClientSettings), testProjectId, testRegion, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -93,18 +95,19 @@ func TestGetInstanceName(t *testing.T) { func TestGetCredentialsUsername(t *testing.T) { tests := []struct { - description string - getCredentialsFails bool - getCredentialsResp *opensearch.CredentialsResponse - isValid bool - expectedOutput string + description string + mockClientSettings mockSettings + isValid bool + expectedOutput string }{ { description: "base", - getCredentialsResp: &opensearch.CredentialsResponse{ - Raw: &opensearch.RawCredentials{ - Credentials: &opensearch.Credentials{ - Username: utils.Ptr(testCredentialsUsername), + mockClientSettings: mockSettings{ + getCredentialsResp: &opensearch.CredentialsResponse{ + Raw: &opensearch.RawCredentials{ + Credentials: opensearch.Credentials{ + Username: testCredentialsUsername, + }, }, }, }, @@ -112,20 +115,17 @@ func TestGetCredentialsUsername(t *testing.T) { expectedOutput: testCredentialsUsername, }, { - description: "get credentials fails", - getCredentialsFails: true, - isValid: false, + description: "get credentials fails", + mockClientSettings: mockSettings{ + getCredentialsFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &openSearchClientMocked{ - getCredentialsFails: tt.getCredentialsFails, - getCredentialsResp: tt.getCredentialsResp, - } - - output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + output, err := GetCredentialsUsername(context.Background(), newAPIClientMock(tt.mockClientSettings), testProjectId, testRegion, testInstanceId, testCredentialsId) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/postgresflex/client/client.go b/internal/pkg/services/postgresflex/client/client.go index ad125cd0c..a40b07aec 100644 --- a/internal/pkg/services/postgresflex/client/client.go +++ b/internal/pkg/services/postgresflex/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*postgresflex.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.PostgresFlexCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := postgresflex.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.PostgresFlexCustomEndpointKey), false, genericclient.CreateApiClient[*postgresflex.APIClient](postgresflex.NewAPIClient)) } diff --git a/internal/pkg/services/postgresflex/utils/utils.go b/internal/pkg/services/postgresflex/utils/utils.go index fc1965bdd..4920feb15 100644 --- a/internal/pkg/services/postgresflex/utils/utils.go +++ b/internal/pkg/services/postgresflex/utils/utils.go @@ -8,20 +8,20 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" "golang.org/x/mod/semver" ) // The number of replicas is enforced by the API according to the instance type -var instanceTypeToReplicas = map[string]int64{ +var instanceTypeToReplicas = map[string]int32{ "Single": 1, "Replica": 3, } type PostgresFlexClient interface { - ListVersionsExecute(ctx context.Context, projectId, region string) (*postgresflex.ListVersionsResponse, error) - GetInstanceExecute(ctx context.Context, projectId, region, instanceId string) (*postgresflex.InstanceResponse, error) - GetUserExecute(ctx context.Context, projectId, region, instanceId, userId string) (*postgresflex.GetUserResponse, error) + ListVersions(ctx context.Context, projectId, region string) postgresflex.ApiListVersionsRequest + GetInstance(ctx context.Context, projectId, region, instanceId string) postgresflex.ApiGetInstanceRequest + GetUser(ctx context.Context, projectId, region, instanceId, userId string) postgresflex.ApiGetUserRequest } func AvailableInstanceTypes() []string { @@ -37,7 +37,7 @@ func AvailableInstanceTypes() []string { return instanceTypes } -func GetInstanceReplicas(instanceType string) (int64, error) { +func GetInstanceReplicas(instanceType string) (int32, error) { numReplicas, ok := instanceTypeToReplicas[instanceType] if !ok { return 0, fmt.Errorf("invalid instance type: %v", instanceType) @@ -45,7 +45,7 @@ func GetInstanceReplicas(instanceType string) (int64, error) { return numReplicas, nil } -func GetInstanceType(numReplicas int64) (string, error) { +func GetInstanceType(numReplicas int32) (string, error) { for k, v := range instanceTypeToReplicas { if v == numReplicas { return k, nil @@ -54,12 +54,12 @@ func GetInstanceType(numReplicas int64) (string, error) { return "", fmt.Errorf("invalid number of replicas: %v", numReplicas) } -func ValidateFlavorId(flavorId string, flavors *[]postgresflex.Flavor) error { +func ValidateFlavorId(flavorId string, flavors []postgresflex.Flavor) error { if flavors == nil { return fmt.Errorf("nil flavors") } - for _, f := range *flavors { + for _, f := range flavors { if f.Id != nil && strings.EqualFold(*f.Id, flavorId) { return nil } @@ -86,7 +86,7 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *postgre return nil } - for _, sc := range *storages.StorageClasses { + for _, sc := range storages.StorageClasses { if strings.EqualFold(*storageClass, sc) { return nil } @@ -98,13 +98,13 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *postgre } } -func LoadFlavorId(cpu, ram int64, flavors *[]postgresflex.Flavor) (*string, error) { +func LoadFlavorId(cpu, ram int64, flavors []postgresflex.Flavor) (*string, error) { if flavors == nil { return nil, fmt.Errorf("nil flavors") } availableFlavors := "" - for _, f := range *flavors { + for _, f := range flavors { if f.Id == nil || f.Cpu == nil || f.Memory == nil { continue } @@ -120,20 +120,19 @@ func LoadFlavorId(cpu, ram int64, flavors *[]postgresflex.Flavor) (*string, erro } func GetLatestPostgreSQLVersion(ctx context.Context, apiClient PostgresFlexClient, projectId, region string) (string, error) { - resp, err := apiClient.ListVersionsExecute(ctx, projectId, region) + resp, err := apiClient.ListVersions(ctx, projectId, region).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL versions: %w", err) } - versions := *resp.Versions latestVersion := "0" - for i := range versions { + for i := range resp.Versions { oldSemVer := fmt.Sprintf("v%s", latestVersion) - newSemVer := fmt.Sprintf("v%s", versions[i]) + newSemVer := fmt.Sprintf("v%s", resp.Versions[i]) if semver.Compare(newSemVer, oldSemVer) != 1 { continue } - latestVersion = versions[i] + latestVersion = resp.Versions[i] } if latestVersion == "0" { return "", fmt.Errorf("no PostgreSQL versions found") @@ -142,7 +141,7 @@ func GetLatestPostgreSQLVersion(ctx context.Context, apiClient PostgresFlexClien } func GetInstanceName(ctx context.Context, apiClient PostgresFlexClient, projectId, region, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, region, instanceId) + resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL Flex instance: %w", err) } @@ -150,7 +149,7 @@ func GetInstanceName(ctx context.Context, apiClient PostgresFlexClient, projectI } func GetInstanceStatus(ctx context.Context, apiClient PostgresFlexClient, projectId, region, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, region, instanceId) + resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL Flex instance: %w", err) } @@ -158,7 +157,7 @@ func GetInstanceStatus(ctx context.Context, apiClient PostgresFlexClient, projec } func GetUserName(ctx context.Context, apiClient PostgresFlexClient, projectId, region, instanceId, userId string) (string, error) { - resp, err := apiClient.GetUserExecute(ctx, projectId, region, instanceId, userId) + resp, err := apiClient.GetUser(ctx, projectId, region, instanceId, userId).Execute() if err != nil { return "", fmt.Errorf("get PostgreSQL Flex user: %w", err) } diff --git a/internal/pkg/services/postgresflex/utils/utils_test.go b/internal/pkg/services/postgresflex/utils/utils_test.go index 346debe49..34a0d4b9c 100644 --- a/internal/pkg/services/postgresflex/utils/utils_test.go +++ b/internal/pkg/services/postgresflex/utils/utils_test.go @@ -9,7 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + postgresflex "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/v2api" ) var ( @@ -25,7 +25,7 @@ const ( testRegion = "eu01" ) -type postgresFlexClientMocked struct { +type mockSettings struct { listVersionsFails bool listVersionsResp *postgresflex.ListVersionsResponse getInstanceFails bool @@ -34,25 +34,27 @@ type postgresFlexClientMocked struct { getUserResp *postgresflex.GetUserResponse } -func (m *postgresFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*postgresflex.ListVersionsResponse, error) { - if m.listVersionsFails { - return nil, fmt.Errorf("could not list versions") - } - return m.listVersionsResp, nil -} - -func (m *postgresFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*postgresflex.InstanceResponse, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *postgresFlexClientMocked) GetUserExecute(_ context.Context, _, _, _, _ string) (*postgresflex.GetUserResponse, error) { - if m.getUserFails { - return nil, fmt.Errorf("could not get user") +func newAPIMockClient(s mockSettings) postgresflex.DefaultAPI { + return postgresflex.DefaultAPIServiceMock{ + ListVersionsExecuteMock: utils.Ptr(func(_ postgresflex.ApiListVersionsRequest) (*postgresflex.ListVersionsResponse, error) { + if s.listVersionsFails { + return nil, fmt.Errorf("could not list versions") + } + return s.listVersionsResp, nil + }), + GetInstanceExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetInstanceRequest) (*postgresflex.InstanceResponse, error) { + if s.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return s.getInstanceResp, nil + }), + GetUserExecuteMock: utils.Ptr(func(_ postgresflex.ApiGetUserRequest) (*postgresflex.GetUserResponse, error) { + if s.getUserFails { + return nil, fmt.Errorf("could not get user") + } + return s.getUserResp, nil + }), } - return m.getUserResp, nil } func TestValidateStorage(t *testing.T) { @@ -68,7 +70,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(10)), storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &postgresflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -88,7 +90,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(1)), storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &postgresflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -101,7 +103,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(200)), storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &postgresflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -114,7 +116,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(5)), storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &postgresflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -127,7 +129,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(20)), storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &postgresflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -140,7 +142,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(10)), storages: &postgresflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "bar-3"}, + StorageClasses: []string{"bar-1", "bar-2", "bar-3"}, StorageRange: &postgresflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -167,13 +169,13 @@ func TestValidateFlavorId(t *testing.T) { tests := []struct { description string flavorId string - flavors *[]postgresflex.Flavor + flavors []postgresflex.Flavor isValid bool }{ { description: "base", flavorId: "foo", - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ {Id: utils.Ptr("bar-1")}, {Id: utils.Ptr("bar-2")}, {Id: utils.Ptr("foo")}, @@ -189,13 +191,13 @@ func TestValidateFlavorId(t *testing.T) { { description: "no flavors", flavorId: "foo", - flavors: &[]postgresflex.Flavor{}, + flavors: []postgresflex.Flavor{}, isValid: false, }, { description: "nil flavor id", flavorId: "foo", - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ {Id: utils.Ptr("bar-1")}, {Id: nil}, {Id: utils.Ptr("foo")}, @@ -205,7 +207,7 @@ func TestValidateFlavorId(t *testing.T) { { description: "invalid flavor", flavorId: "foo", - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ {Id: utils.Ptr("bar-1")}, {Id: utils.Ptr("bar-2")}, {Id: utils.Ptr("bar-3")}, @@ -232,7 +234,7 @@ func TestLoadFlavorId(t *testing.T) { description string cpu int64 ram int64 - flavors *[]postgresflex.Flavor + flavors []postgresflex.Flavor isValid bool expectedOutput *string }{ @@ -240,7 +242,7 @@ func TestLoadFlavorId(t *testing.T) { description: "base", cpu: 2, ram: 4, - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ { Id: utils.Ptr("bar-1"), Cpu: utils.Ptr(int64(2)), @@ -271,14 +273,14 @@ func TestLoadFlavorId(t *testing.T) { description: "no flavors", cpu: 2, ram: 4, - flavors: &[]postgresflex.Flavor{}, + flavors: []postgresflex.Flavor{}, isValid: false, }, { description: "flavors with details missing", cpu: 2, ram: 4, - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ { Id: utils.Ptr("bar-1"), Cpu: nil, @@ -302,7 +304,7 @@ func TestLoadFlavorId(t *testing.T) { description: "match with nil id", cpu: 2, ram: 4, - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ { Id: utils.Ptr("bar-1"), Cpu: utils.Ptr(int64(2)), @@ -325,7 +327,7 @@ func TestLoadFlavorId(t *testing.T) { description: "invalid settings", cpu: 2, ram: 4, - flavors: &[]postgresflex.Flavor{ + flavors: []postgresflex.Flavor{ { Id: utils.Ptr("bar-1"), Cpu: utils.Ptr(int64(2)), @@ -368,29 +370,34 @@ func TestLoadFlavorId(t *testing.T) { func TestGetLatestPostgreSQLVersion(t *testing.T) { tests := []struct { - description string - listVersionsFails bool - listVersionsResp *postgresflex.ListVersionsResponse - isValid bool - expectedOutput string + description string + mockClientSettings mockSettings + isValid bool + expectedOutput string }{ { description: "base", - listVersionsResp: &postgresflex.ListVersionsResponse{ - Versions: &[]string{"8", "10", "9"}, + mockClientSettings: mockSettings{ + listVersionsResp: &postgresflex.ListVersionsResponse{ + Versions: []string{"8", "10", "9"}, + }, }, isValid: true, expectedOutput: "10", }, { - description: "get instance fails", - listVersionsFails: true, - isValid: false, + description: "get instance fails", + mockClientSettings: mockSettings{ + listVersionsFails: true, + }, + isValid: false, }, { description: "no versions", - listVersionsResp: &postgresflex.ListVersionsResponse{ - Versions: &[]string{}, + mockClientSettings: mockSettings{ + listVersionsResp: &postgresflex.ListVersionsResponse{ + Versions: []string{}, + }, }, isValid: false, }, @@ -398,12 +405,7 @@ func TestGetLatestPostgreSQLVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - listVersionsFails: tt.listVersionsFails, - listVersionsResp: tt.listVersionsResp, - } - - output, err := GetLatestPostgreSQLVersion(context.Background(), client, testProjectId, testRegion) + output, err := GetLatestPostgreSQLVersion(context.Background(), newAPIMockClient(tt.mockClientSettings), testProjectId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -423,37 +425,35 @@ func TestGetLatestPostgreSQLVersion(t *testing.T) { func TestGetInstanceName(t *testing.T) { tests := []struct { - description string - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse - isValid bool - expectedOutput string + description string + mockClientSettings mockSettings + isValid bool + expectedOutput string }{ { description: "base", - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Name: utils.Ptr(testInstanceName), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Name: utils.Ptr(testInstanceName), + }, }, }, isValid: true, expectedOutput: testInstanceName, }, { - description: "get instance fails", - getInstanceFails: true, - isValid: false, + description: "get instance fails", + mockClientSettings: mockSettings{ + getInstanceFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - getInstanceFails: tt.getInstanceFails, - getInstanceResp: tt.getInstanceResp, - } - - output, err := GetInstanceName(context.Background(), client, testProjectId, testRegion, testInstanceId) + output, err := GetInstanceName(context.Background(), newAPIMockClient(tt.mockClientSettings), testProjectId, testRegion, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -473,37 +473,35 @@ func TestGetInstanceName(t *testing.T) { func TestGetInstanceStatus(t *testing.T) { tests := []struct { - description string - getInstanceFails bool - getInstanceResp *postgresflex.InstanceResponse - isValid bool - expectedOutput string + description string + mockClientSettings mockSettings + isValid bool + expectedOutput string }{ { description: "base", - getInstanceResp: &postgresflex.InstanceResponse{ - Item: &postgresflex.Instance{ - Status: utils.Ptr(testStatus), + mockClientSettings: mockSettings{ + getInstanceResp: &postgresflex.InstanceResponse{ + Item: &postgresflex.Instance{ + Status: utils.Ptr(testStatus), + }, }, }, isValid: true, expectedOutput: testStatus, }, { - description: "get instance fails", - getInstanceFails: true, - isValid: false, + description: "get instance fails", + mockClientSettings: mockSettings{ + getInstanceFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - getInstanceFails: tt.getInstanceFails, - getInstanceResp: tt.getInstanceResp, - } - - output, err := GetInstanceStatus(context.Background(), client, testProjectId, testRegion, testInstanceId) + output, err := GetInstanceStatus(context.Background(), newAPIMockClient(tt.mockClientSettings), testProjectId, testRegion, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -523,37 +521,35 @@ func TestGetInstanceStatus(t *testing.T) { func TestGetUserName(t *testing.T) { tests := []struct { - description string - getUserFails bool - getUserResp *postgresflex.GetUserResponse - isValid bool - expectedOutput string + description string + mockClientSettings mockSettings + isValid bool + expectedOutput string }{ { description: "base", - getUserResp: &postgresflex.GetUserResponse{ - Item: &postgresflex.UserResponse{ - Username: utils.Ptr(testUserName), + mockClientSettings: mockSettings{ + getUserResp: &postgresflex.GetUserResponse{ + Item: &postgresflex.UserResponse{ + Username: utils.Ptr(testUserName), + }, }, }, isValid: true, expectedOutput: testUserName, }, { - description: "get user fails", - getUserFails: true, - isValid: false, + description: "get user fails", + mockClientSettings: mockSettings{ + getUserFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &postgresFlexClientMocked{ - getUserFails: tt.getUserFails, - getUserResp: tt.getUserResp, - } - - output, err := GetUserName(context.Background(), client, testProjectId, testRegion, testInstanceId, testUserId) + output, err := GetUserName(context.Background(), newAPIMockClient(tt.mockClientSettings), testProjectId, testRegion, testInstanceId, testUserId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -574,7 +570,7 @@ func TestGetUserName(t *testing.T) { func TestGetInstanceType(t *testing.T) { tests := []struct { description string - numReplicas int64 + numReplicas int32 expectedOutput string isValid bool }{ diff --git a/internal/pkg/services/rabbitmq/client/client.go b/internal/pkg/services/rabbitmq/client/client.go index 90c4df9fa..999a2446a 100644 --- a/internal/pkg/services/rabbitmq/client/client.go +++ b/internal/pkg/services/rabbitmq/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*rabbitmq.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.RabbitMQCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := rabbitmq.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.RabbitMQCustomEndpointKey), false, genericclient.CreateApiClient[*rabbitmq.APIClient](rabbitmq.NewAPIClient)) } diff --git a/internal/pkg/services/rabbitmq/utils/utils.go b/internal/pkg/services/rabbitmq/utils/utils.go index 6eced8759..4d33c2cfb 100644 --- a/internal/pkg/services/rabbitmq/utils/utils.go +++ b/internal/pkg/services/rabbitmq/utils/utils.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) const ( @@ -15,9 +15,9 @@ const ( ) func ValidatePlanId(planId string, offerings *rabbitmq.ListOfferingsResponse) error { - for _, offer := range *offerings.Offerings { - for _, plan := range *offer.Plans { - if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + for _, offer := range offerings.GetOfferings() { + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Id, planId) { return nil } } @@ -33,21 +33,18 @@ func LoadPlanId(planName, version string, offerings *rabbitmq.ListOfferingsRespo availableVersions := "" availablePlanNames := "" isValidVersion := false - for _, offer := range *offerings.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) + for _, offer := range offerings.GetOfferings() { + if !strings.EqualFold(offer.Version, version) { + availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, offer.Version) continue } isValidVersion = true - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Name, planName) { + return &plan.Id, nil } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - return plan.Id, nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, plan.Name) } } @@ -65,23 +62,18 @@ func LoadPlanId(planName, version string, offerings *rabbitmq.ListOfferingsRespo } } -type RabbitMQClient interface { - GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*rabbitmq.Instance, error) - GetCredentialsExecute(ctx context.Context, projectId, instanceId, credentialsId string) (*rabbitmq.CredentialsResponse, error) -} - -func GetInstanceName(ctx context.Context, apiClient RabbitMQClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) +func GetInstanceName(ctx context.Context, apiClient rabbitmq.DefaultAPI, projectId, region, instanceId string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { return "", fmt.Errorf("get RabbitMQ instance: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetCredentialsUsername(ctx context.Context, apiClient RabbitMQClient, projectId, instanceId, credentialsId string) (string, error) { - resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) +func GetCredentialsUsername(ctx context.Context, apiClient rabbitmq.DefaultAPI, projectId, region, instanceId, credentialsId string) (string, error) { + resp, err := apiClient.GetCredentials(ctx, projectId, region, instanceId, credentialsId).Execute() if err != nil { return "", fmt.Errorf("get RabbitMQ credentials: %w", err) } - return *resp.Raw.Credentials.Username, nil + return resp.Raw.Credentials.Username, nil } diff --git a/internal/pkg/services/rabbitmq/utils/utils_test.go b/internal/pkg/services/rabbitmq/utils/utils_test.go index 7c468c02c..7939cb86b 100644 --- a/internal/pkg/services/rabbitmq/utils/utils_test.go +++ b/internal/pkg/services/rabbitmq/utils/utils_test.go @@ -8,7 +8,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" ) var ( @@ -19,28 +19,32 @@ var ( const ( testInstanceName = "instance" + testRegion = "eu01" testCredentialsUsername = "username" ) -type rabbitMQClientMocked struct { +type rabbitMQClientMockSettings struct { getInstanceFails bool getInstanceResp *rabbitmq.Instance getCredentialsFails bool getCredentialsResp *rabbitmq.CredentialsResponse } -func (m *rabbitMQClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*rabbitmq.Instance, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *rabbitMQClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*rabbitmq.CredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get user") +func newApiMock(s *rabbitMQClientMockSettings) rabbitmq.DefaultAPI { + return &rabbitmq.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ rabbitmq.ApiGetInstanceRequest) (*rabbitmq.Instance, error) { + if s.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return s.getInstanceResp, nil + }), + GetCredentialsExecuteMock: utils.Ptr(func(_ rabbitmq.ApiGetCredentialsRequest) (*rabbitmq.CredentialsResponse, error) { + if s.getCredentialsFails { + return nil, fmt.Errorf("could not get user") + } + return s.getCredentialsResp, nil + }), } - return m.getCredentialsResp, nil } func TestGetInstanceName(t *testing.T) { @@ -54,7 +58,7 @@ func TestGetInstanceName(t *testing.T) { { description: "base", getInstanceResp: &rabbitmq.Instance{ - Name: utils.Ptr(testInstanceName), + Name: testInstanceName, }, isValid: true, expectedOutput: testInstanceName, @@ -68,12 +72,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &rabbitMQClientMocked{ + settings := &rabbitMQClientMockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + output, err := GetInstanceName(context.Background(), newApiMock(settings), testProjectId, testRegion, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -103,8 +107,8 @@ func TestGetCredentialsUsername(t *testing.T) { description: "base", getCredentialsResp: &rabbitmq.CredentialsResponse{ Raw: &rabbitmq.RawCredentials{ - Credentials: &rabbitmq.Credentials{ - Username: utils.Ptr(testCredentialsUsername), + Credentials: rabbitmq.Credentials{ + Username: testCredentialsUsername, }, }, }, @@ -120,12 +124,12 @@ func TestGetCredentialsUsername(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &rabbitMQClientMocked{ + settings := &rabbitMQClientMockSettings{ getCredentialsFails: tt.getCredentialsFails, getCredentialsResp: tt.getCredentialsResp, } - output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + output, err := GetCredentialsUsername(context.Background(), newApiMock(settings), testProjectId, testRegion, testInstanceId, testCredentialsId) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/redis/client/client.go b/internal/pkg/services/redis/client/client.go index 1966b0ad2..c26718153 100644 --- a/internal/pkg/services/redis/client/client.go +++ b/internal/pkg/services/redis/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*redis.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.RedisCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := redis.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.RedisCustomEndpointKey), false, genericclient.CreateApiClient[*redis.APIClient](redis.NewAPIClient)) } diff --git a/internal/pkg/services/redis/utils/utils.go b/internal/pkg/services/redis/utils/utils.go index c20444a6c..6a21566c5 100644 --- a/internal/pkg/services/redis/utils/utils.go +++ b/internal/pkg/services/redis/utils/utils.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" ) const ( @@ -15,9 +15,9 @@ const ( ) func ValidatePlanId(planId string, offerings *redis.ListOfferingsResponse) error { - for _, offer := range *offerings.Offerings { - for _, plan := range *offer.Plans { - if plan.Id != nil && strings.EqualFold(*plan.Id, planId) { + for _, offer := range offerings.GetOfferings() { + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Id, planId) { return nil } } @@ -33,21 +33,18 @@ func LoadPlanId(planName, version string, offerings *redis.ListOfferingsResponse availableVersions := "" availablePlanNames := "" isValidVersion := false - for _, offer := range *offerings.Offerings { - if !strings.EqualFold(*offer.Version, version) { - availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) + for _, offer := range offerings.GetOfferings() { + if !strings.EqualFold(offer.Version, version) { + availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, offer.Version) continue } isValidVersion = true - for _, plan := range *offer.Plans { - if plan.Name == nil { - continue + for _, plan := range offer.Plans { + if strings.EqualFold(plan.Name, planName) { + return &plan.Id, nil } - if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { - return plan.Id, nil - } - availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, plan.Name) } } @@ -65,23 +62,18 @@ func LoadPlanId(planName, version string, offerings *redis.ListOfferingsResponse } } -type RedisClient interface { - GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*redis.Instance, error) - GetCredentialsExecute(ctx context.Context, projectId, instanceId, credentialsId string) (*redis.CredentialsResponse, error) -} - -func GetInstanceName(ctx context.Context, apiClient RedisClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) +func GetInstanceName(ctx context.Context, apiClient redis.DefaultAPI, projectId, instanceId, region string) (string, error) { + resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { return "", fmt.Errorf("get Redis instance: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetCredentialsUsername(ctx context.Context, apiClient RedisClient, projectId, instanceId, credentialsId string) (string, error) { - resp, err := apiClient.GetCredentialsExecute(ctx, projectId, instanceId, credentialsId) +func GetCredentialsUsername(ctx context.Context, apiClient redis.DefaultAPI, projectId, instanceId, credentialsId, region string) (string, error) { + resp, err := apiClient.GetCredentials(ctx, projectId, region, instanceId, credentialsId).Execute() if err != nil { return "", fmt.Errorf("get Redis credentials: %w", err) } - return *resp.Raw.Credentials.Username, nil + return resp.Raw.Credentials.Username, nil } diff --git a/internal/pkg/services/redis/utils/utils_test.go b/internal/pkg/services/redis/utils/utils_test.go index 83b00eb1a..6b8d3ea7f 100644 --- a/internal/pkg/services/redis/utils/utils_test.go +++ b/internal/pkg/services/redis/utils/utils_test.go @@ -5,10 +5,10 @@ import ( "fmt" "testing" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/redis" + redis "github.com/stackitcloud/stackit-sdk-go/services/redis/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) var ( @@ -20,27 +20,33 @@ var ( const ( testInstanceName = "instance" testCredentialsUsername = "username" + testRegion = "eu01" ) -type redisClientMocked struct { +type mockSettings struct { getInstanceFails bool getInstanceResp *redis.Instance getCredentialsFails bool getCredentialsResp *redis.CredentialsResponse } -func (m *redisClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*redis.Instance, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} +func newAPIMock(settings *mockSettings) redis.DefaultAPI { + return &redis.DefaultAPIServiceMock{ + GetInstanceExecuteMock: utils.Ptr(func(_ redis.ApiGetInstanceRequest) (*redis.Instance, error) { + if settings.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + + return settings.getInstanceResp, nil + }), + GetCredentialsExecuteMock: utils.Ptr(func(_ redis.ApiGetCredentialsRequest) (*redis.CredentialsResponse, error) { + if settings.getCredentialsFails { + return nil, fmt.Errorf("could not get user") + } -func (m *redisClientMocked) GetCredentialsExecute(_ context.Context, _, _, _ string) (*redis.CredentialsResponse, error) { - if m.getCredentialsFails { - return nil, fmt.Errorf("could not get user") + return settings.getCredentialsResp, nil + }), } - return m.getCredentialsResp, nil } func TestGetInstanceName(t *testing.T) { @@ -54,7 +60,7 @@ func TestGetInstanceName(t *testing.T) { { description: "base", getInstanceResp: &redis.Instance{ - Name: utils.Ptr(testInstanceName), + Name: testInstanceName, }, isValid: true, expectedOutput: testInstanceName, @@ -68,12 +74,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &redisClientMocked{ + client := newAPIMock(&mockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, - } + }) - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -103,8 +109,8 @@ func TestGetCredentialsUsername(t *testing.T) { description: "base", getCredentialsResp: &redis.CredentialsResponse{ Raw: &redis.RawCredentials{ - Credentials: &redis.Credentials{ - Username: utils.Ptr(testCredentialsUsername), + Credentials: redis.Credentials{ + Username: testCredentialsUsername, }, }, }, @@ -120,12 +126,12 @@ func TestGetCredentialsUsername(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &redisClientMocked{ + client := newAPIMock(&mockSettings{ getCredentialsFails: tt.getCredentialsFails, getCredentialsResp: tt.getCredentialsResp, - } + }) - output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId) + output, err := GetCredentialsUsername(context.Background(), client, testProjectId, testInstanceId, testCredentialsId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/resourcemanager/client/client.go b/internal/pkg/services/resourcemanager/client/client.go index 5cca3a34b..a430b059e 100644 --- a/internal/pkg/services/resourcemanager/client/client.go +++ b/internal/pkg/services/resourcemanager/client/client.go @@ -1,45 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*resourcemanager.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ResourceManagerEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := resourcemanager.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ResourceManagerEndpointKey), false, genericclient.CreateApiClient[*resourcemanager.APIClient](resourcemanager.NewAPIClient)) } diff --git a/internal/pkg/services/resourcemanager/utils/utils.go b/internal/pkg/services/resourcemanager/utils/utils.go index bf8724b06..b6144e4e2 100644 --- a/internal/pkg/services/resourcemanager/utils/utils.go +++ b/internal/pkg/services/resourcemanager/utils/utils.go @@ -4,29 +4,24 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" ) -type ResourceManagerClient interface { - GetOrganizationExecute(ctx context.Context, organizationId string) (*resourcemanager.OrganizationResponse, error) - GetProjectExecute(ctx context.Context, projectId string) (*resourcemanager.GetProjectResponse, error) -} - // GetOrganizationName returns the name of an organization by its ID. -func GetOrganizationName(ctx context.Context, apiClient ResourceManagerClient, orgId string) (string, error) { - resp, err := apiClient.GetOrganizationExecute(ctx, orgId) +func GetOrganizationName(ctx context.Context, apiClient resourcemanager.DefaultAPI, orgId string) (string, error) { + resp, err := apiClient.GetOrganization(ctx, orgId).Execute() if err != nil { return "", fmt.Errorf("get organization details: %w", err) } - return *resp.Name, nil + return resp.Name, nil } -func GetProjectName(ctx context.Context, apiClient ResourceManagerClient, projectId string) (string, error) { - resp, err := apiClient.GetProjectExecute(ctx, projectId) +func GetProjectName(ctx context.Context, apiClient resourcemanager.DefaultAPI, projectId string) (string, error) { + resp, err := apiClient.GetProject(ctx, projectId).Execute() if err != nil { return "", fmt.Errorf("get project details: %w", err) } - return *resp.Name, nil + return resp.Name, nil } diff --git a/internal/pkg/services/resourcemanager/utils/utils_test.go b/internal/pkg/services/resourcemanager/utils/utils_test.go index bcd0ad2d0..ec53935a7 100644 --- a/internal/pkg/services/resourcemanager/utils/utils_test.go +++ b/internal/pkg/services/resourcemanager/utils/utils_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/google/uuid" + resourcemanager "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/v0api" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" ) var ( @@ -25,18 +26,21 @@ type resourceManagerClientMocked struct { getProjectResp *resourcemanager.GetProjectResponse } -func (s *resourceManagerClientMocked) GetOrganizationExecute(_ context.Context, _ string) (*resourcemanager.OrganizationResponse, error) { - if s.getOrganizationFails { - return nil, fmt.Errorf("could not get organization") - } - return s.getOrganizationResp, nil -} - -func (s *resourceManagerClientMocked) GetProjectExecute(_ context.Context, _ string) (*resourcemanager.GetProjectResponse, error) { - if s.getProjectFails { - return nil, fmt.Errorf("could not get project") +func (s *resourceManagerClientMocked) newMock() resourcemanager.DefaultAPI { + return resourcemanager.DefaultAPIServiceMock{ + GetOrganizationExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetOrganizationRequest) (*resourcemanager.OrganizationResponse, error) { + if s.getOrganizationFails { + return nil, fmt.Errorf("could not get organization") + } + return s.getOrganizationResp, nil + }), + GetProjectExecuteMock: utils.Ptr(func(_ resourcemanager.ApiGetProjectRequest) (*resourcemanager.GetProjectResponse, error) { + if s.getProjectFails { + return nil, fmt.Errorf("could not get project") + } + return s.getProjectResp, nil + }), } - return s.getProjectResp, nil } func TestGetOrganizationName(t *testing.T) { @@ -50,7 +54,7 @@ func TestGetOrganizationName(t *testing.T) { { description: "base", getOrganizationResp: &resourcemanager.OrganizationResponse{ - Name: utils.Ptr(testOrgName), + Name: testOrgName, }, isValid: true, expectedOutput: testOrgName, @@ -69,7 +73,7 @@ func TestGetOrganizationName(t *testing.T) { getOrganizationResp: tt.getOrganizationResp, } - output, err := GetOrganizationName(context.Background(), client, testOrgId) + output, err := GetOrganizationName(context.Background(), client.newMock(), testOrgId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -98,7 +102,7 @@ func TestGetProjectName(t *testing.T) { { description: "base", getProjectResp: &resourcemanager.GetProjectResponse{ - Name: utils.Ptr("project"), + Name: "project", }, isValid: true, expectedOutput: "project", @@ -117,7 +121,7 @@ func TestGetProjectName(t *testing.T) { getProjectResp: tt.getProjectResp, } - output, err := GetProjectName(context.Background(), client, testOrgId) + output, err := GetProjectName(context.Background(), client.newMock(), testOrgId) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/runcommand/client/client.go b/internal/pkg/services/runcommand/client/client.go index 86f6d3d5d..f1c3cd94b 100644 --- a/internal/pkg/services/runcommand/client/client.go +++ b/internal/pkg/services/runcommand/client/client.go @@ -1,46 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/runcommand" + runcommand "github.com/stackitcloud/stackit-sdk-go/services/runcommand/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*runcommand.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.RunCommandCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } else { - cfgOptions = append(cfgOptions, authCfgOption) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := runcommand.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.RunCommandCustomEndpointKey), false, genericclient.CreateApiClient[*runcommand.APIClient](runcommand.NewAPIClient)) } diff --git a/internal/pkg/services/runcommand/utils/utils.go b/internal/pkg/services/runcommand/utils/utils.go index d6775f373..358e27c59 100644 --- a/internal/pkg/services/runcommand/utils/utils.go +++ b/internal/pkg/services/runcommand/utils/utils.go @@ -5,13 +5,15 @@ import ( "strings" ) -func ParseScriptParams(params map[string]string) (map[string]string, error) { +func ParseScriptParams(params *map[string]string) (*map[string]string, error) { //nolint:gocritic // flag value is a map pointer if params == nil { return nil, nil } + parsed := map[string]string{} - for k, v := range params { + for k, v := range *params { parsed[k] = v + if k == "script" && strings.HasPrefix(v, "@{") && strings.HasSuffix(v, "}") { // Check if a script file path was specified, like: --params script=@{/tmp/test.sh} fileContents, err := os.ReadFile(v[2 : len(v)-1]) @@ -21,5 +23,6 @@ func ParseScriptParams(params map[string]string) (map[string]string, error) { parsed[k] = string(fileContents) } } - return parsed, nil + + return &parsed, nil } diff --git a/internal/pkg/services/runcommand/utils/utils_test.go b/internal/pkg/services/runcommand/utils/utils_test.go index 5b1d1c69f..320bac18c 100644 --- a/internal/pkg/services/runcommand/utils/utils_test.go +++ b/internal/pkg/services/runcommand/utils/utils_test.go @@ -2,26 +2,34 @@ package utils import ( "testing" + + "github.com/google/go-cmp/cmp" ) func TestParseScriptParams(t *testing.T) { tests := []struct { description string - input map[string]string - expectedOutput map[string]string + input *map[string]string + expectedOutput *map[string]string isValid bool }{ { - "base-ok", - map[string]string{"script": "ls /"}, - map[string]string{"script": "ls /"}, - true, + description: "base-ok", + input: &map[string]string{"script": "ls /"}, + expectedOutput: &map[string]string{"script": "ls /"}, + isValid: true, + }, + { + description: "nil input", + input: nil, + expectedOutput: nil, + isValid: true, }, { - "not-ok-nonexistant-file-specified-for-script", - map[string]string{"script": "@{/some/file/which/does/not/exist/and/thus/fails}"}, - nil, - false, + description: "not-ok-nonexistant-file-specified-for-script", + input: &map[string]string{"script": "@{/some/file/which/does/not/exist/and/thus/fails}"}, + expectedOutput: nil, + isValid: false, }, } @@ -38,8 +46,9 @@ func TestParseScriptParams(t *testing.T) { if !tt.isValid { return } - if output["script"] != tt.expectedOutput["script"] { - t.Errorf("expected output to be %s, got %s", tt.expectedOutput["script"], output["script"]) + diff := cmp.Diff(output, tt.expectedOutput) + if diff != "" { + t.Fatalf("ParseScriptParams() output mismatch (-want +got):\n%s", diff) } }) } diff --git a/internal/pkg/services/secrets-manager/client/client.go b/internal/pkg/services/secrets-manager/client/client.go index 3ef6e1402..1f2350556 100644 --- a/internal/pkg/services/secrets-manager/client/client.go +++ b/internal/pkg/services/secrets-manager/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*secretsmanager.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - region := viper.GetString(config.RegionKey) - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - sdkConfig.WithRegion(region), - authCfgOption, - } - - customEndpoint := viper.GetString(config.SecretsManagerCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := secretsmanager.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.SecretsManagerCustomEndpointKey), true, genericclient.CreateApiClient[*secretsmanager.APIClient](secretsmanager.NewAPIClient)) } diff --git a/internal/pkg/services/secrets-manager/utils/utils.go b/internal/pkg/services/secrets-manager/utils/utils.go index 0b53f64f5..1bbd315e0 100644 --- a/internal/pkg/services/secrets-manager/utils/utils.go +++ b/internal/pkg/services/secrets-manager/utils/utils.go @@ -4,38 +4,38 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" ) type SecretsManagerClient interface { - GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*secretsmanager.Instance, error) - GetUserExecute(ctx context.Context, projectId string, instanceId string, userId string) (*secretsmanager.User, error) + GetInstance(ctx context.Context, projectId, instanceId string) secretsmanager.ApiGetInstanceRequest + GetUser(ctx context.Context, projectId string, instanceId string, userId string) secretsmanager.ApiGetUserRequest } func GetInstanceName(ctx context.Context, apiClient SecretsManagerClient, projectId, instanceId string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId) + resp, err := apiClient.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { return "", fmt.Errorf("get Secrets Manager instance: %w", err) } - return *resp.Name, nil + return resp.Name, nil } func GetUserLabel(ctx context.Context, apiClient SecretsManagerClient, projectId, instanceId, userId string) (string, error) { - resp, err := apiClient.GetUserExecute(ctx, projectId, instanceId, userId) + resp, err := apiClient.GetUser(ctx, projectId, instanceId, userId).Execute() if err != nil { return "", fmt.Errorf("get Secrets Manager user: %w", err) } - if resp.Username == nil || *resp.Username == "" { + if resp.Username == "" { // Should never happen, username is auto-generated return "", fmt.Errorf("username is empty") } var userLabel string - if resp.Description == nil || *resp.Description == "" { - userLabel = fmt.Sprintf("%q", *resp.Username) + if resp.Description == "" { + userLabel = fmt.Sprintf("%q", resp.Username) } else { - userLabel = fmt.Sprintf("%q (%s)", *resp.Username, *resp.Description) + userLabel = fmt.Sprintf("%q (%s)", resp.Username, resp.Description) } return userLabel, nil } diff --git a/internal/pkg/services/secrets-manager/utils/utils_test.go b/internal/pkg/services/secrets-manager/utils/utils_test.go index d79ca49f7..92f4ab9f6 100644 --- a/internal/pkg/services/secrets-manager/utils/utils_test.go +++ b/internal/pkg/services/secrets-manager/utils/utils_test.go @@ -6,8 +6,9 @@ import ( "testing" "github.com/google/uuid" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) var ( @@ -22,25 +23,30 @@ const ( testDescription = "sample description" ) -type secretsManagerClientMocked struct { +type apiClientMockOptions struct { getInstanceFails bool getInstanceResp *secretsmanager.Instance getUserFails bool getUserResp *secretsmanager.User } -func (s *secretsManagerClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*secretsmanager.Instance, error) { - if s.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return s.getInstanceResp, nil -} - -func (s *secretsManagerClientMocked) GetUserExecute(_ context.Context, _, _, _ string) (*secretsmanager.User, error) { - if s.getUserFails { - return nil, fmt.Errorf("could not get user") +func newAPIClient(options apiClientMockOptions) secretsmanager.APIClient { + return secretsmanager.APIClient{ + DefaultAPI: secretsmanager.DefaultAPIServiceMock{ + GetUserExecuteMock: utils.Ptr(func(_ secretsmanager.ApiGetUserRequest) (*secretsmanager.User, error) { + if options.getUserFails { + return nil, fmt.Errorf("could not get user") + } + return options.getUserResp, nil + }), + GetInstanceExecuteMock: utils.Ptr(func(_ secretsmanager.ApiGetInstanceRequest) (*secretsmanager.Instance, error) { + if options.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return options.getInstanceResp, nil + }), + }, } - return s.getUserResp, nil } func TestGetInstanceName(t *testing.T) { @@ -54,7 +60,7 @@ func TestGetInstanceName(t *testing.T) { { description: "base", getInstanceResp: &secretsmanager.Instance{ - Name: utils.Ptr(testInstanceName), + Name: testInstanceName, }, isValid: true, expectedOutput: testInstanceName, @@ -68,12 +74,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &secretsManagerClientMocked{ + mockOptions := apiClientMockOptions{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId) + output, err := GetInstanceName(context.Background(), newAPIClient(mockOptions).DefaultAPI, testProjectId, testInstanceId) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -102,8 +108,8 @@ func TestGetUserDetails(t *testing.T) { { description: "base", GetUserResp: &secretsmanager.User{ - Username: utils.Ptr(testUserName), - Description: utils.Ptr(testDescription), + Username: testUserName, + Description: testDescription, }, isValid: true, expectedOutput: fmt.Sprintf("%q (%s)", testUserName, testDescription), @@ -111,7 +117,7 @@ func TestGetUserDetails(t *testing.T) { { description: "user has no description", GetUserResp: &secretsmanager.User{ - Username: utils.Ptr(testUserName), + Username: testUserName, }, isValid: true, expectedOutput: fmt.Sprintf("%q", testUserName), @@ -119,8 +125,8 @@ func TestGetUserDetails(t *testing.T) { { description: "user has empty description", GetUserResp: &secretsmanager.User{ - Username: utils.Ptr(testUserName), - Description: utils.Ptr(""), + Username: testUserName, + Description: "", }, isValid: true, expectedOutput: fmt.Sprintf("%q", testUserName), @@ -128,14 +134,14 @@ func TestGetUserDetails(t *testing.T) { { description: "user has empty username", GetUserResp: &secretsmanager.User{ - Username: utils.Ptr(""), + Username: "", }, isValid: false, }, { description: "user has no username", GetUserResp: &secretsmanager.User{ - Username: nil, + Username: "", }, isValid: false, }, @@ -148,12 +154,12 @@ func TestGetUserDetails(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &secretsManagerClientMocked{ + options := &apiClientMockOptions{ getUserFails: tt.getUserFails, getUserResp: tt.GetUserResp, } - userLabel, err := GetUserLabel(context.Background(), client, testProjectId, testInstanceId, testUserId) + userLabel, err := GetUserLabel(context.Background(), newAPIClient(*options).DefaultAPI, testProjectId, testInstanceId, testUserId) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/serverbackup/client/client.go b/internal/pkg/services/serverbackup/client/client.go index 1184ce684..2bc42f42f 100644 --- a/internal/pkg/services/serverbackup/client/client.go +++ b/internal/pkg/services/serverbackup/client/client.go @@ -1,47 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*serverbackup.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ServerBackupCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } else { - region := viper.GetString(config.RegionKey) - cfgOptions = append(cfgOptions, authCfgOption, sdkConfig.WithRegion(region)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := serverbackup.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ServerBackupCustomEndpointKey), false, genericclient.CreateApiClient[*serverbackup.APIClient](serverbackup.NewAPIClient)) } diff --git a/internal/pkg/services/serverbackup/utils/utils.go b/internal/pkg/services/serverbackup/utils/utils.go index c6974a414..c74c3fa09 100644 --- a/internal/pkg/services/serverbackup/utils/utils.go +++ b/internal/pkg/services/serverbackup/utils/utils.go @@ -4,28 +4,23 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) -type ServerBackupClient interface { - ListBackupSchedulesExecute(ctx context.Context, projectId, serverId, region string) (*serverbackup.GetBackupSchedulesResponse, error) - ListBackupsExecute(ctx context.Context, projectId, serverId, region string) (*serverbackup.GetBackupsListResponse, error) -} - -func CanDisableBackupService(ctx context.Context, client ServerBackupClient, projectId, serverId, region string) (bool, error) { - schedules, err := client.ListBackupSchedulesExecute(ctx, projectId, serverId, region) +func CanDisableBackupService(ctx context.Context, client serverbackup.DefaultAPI, projectId, serverId, region string) (bool, error) { + schedules, err := client.ListBackupSchedules(ctx, projectId, serverId, region).Execute() if err != nil { return false, fmt.Errorf("list backup schedules: %w", err) } - if *schedules.Items != nil && len(*schedules.Items) > 0 { + if len(schedules.Items) > 0 { return false, nil } - backups, err := client.ListBackupsExecute(ctx, projectId, serverId, region) + backups, err := client.ListBackups(ctx, projectId, serverId, region).Execute() if err != nil { return false, fmt.Errorf("list backups: %w", err) } - if *backups.Items != nil && len(*backups.Items) > 0 { + if len(backups.Items) > 0 { return false, nil } diff --git a/internal/pkg/services/serverbackup/utils/utils_test.go b/internal/pkg/services/serverbackup/utils/utils_test.go index 73b915b3a..23a8df1d1 100644 --- a/internal/pkg/services/serverbackup/utils/utils_test.go +++ b/internal/pkg/services/serverbackup/utils/utils_test.go @@ -8,108 +8,119 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + serverbackup "github.com/stackitcloud/stackit-sdk-go/services/serverbackup/v2api" ) +const testRegion = "eu01" + var ( testProjectId = uuid.NewString() testServerId = uuid.NewString() - testRegion = "eu01" ) -type serverbackupClientMocked struct { +type mockSettings struct { listBackupSchedulesFails bool listBackupSchedulesResp *serverbackup.GetBackupSchedulesResponse listBackupsFails bool listBackupsResp *serverbackup.GetBackupsListResponse } -func (m *serverbackupClientMocked) ListBackupSchedulesExecute(_ context.Context, _, _, _ string) (*serverbackup.GetBackupSchedulesResponse, error) { - if m.listBackupSchedulesFails { - return nil, fmt.Errorf("could not list backup schedules") - } - return m.listBackupSchedulesResp, nil -} - -func (m *serverbackupClientMocked) ListBackupsExecute(_ context.Context, _, _, _ string) (*serverbackup.GetBackupsListResponse, error) { - if m.listBackupsFails { - return nil, fmt.Errorf("could not list backups") +func newServerbackupClientMock(s mockSettings) serverbackup.DefaultAPI { + return &serverbackup.DefaultAPIServiceMock{ + ListBackupSchedulesExecuteMock: utils.Ptr(func(_ serverbackup.ApiListBackupSchedulesRequest) (*serverbackup.GetBackupSchedulesResponse, error) { + if s.listBackupSchedulesFails { + return nil, fmt.Errorf("could not list backup schedules") + } + return s.listBackupSchedulesResp, nil + }), + ListBackupsExecuteMock: utils.Ptr(func(_ serverbackup.ApiListBackupsRequest) (*serverbackup.GetBackupsListResponse, error) { + if s.listBackupsFails { + return nil, fmt.Errorf("could not list backups") + } + return s.listBackupsResp, nil + }), } - return m.listBackupsResp, nil } func TestCanDisableBackupService(t *testing.T) { tests := []struct { - description string - listBackupsFails bool - listBackupSchedulesFails bool - listBackups *serverbackup.GetBackupsListResponse - listBackupSchedules *serverbackup.GetBackupSchedulesResponse - isValid bool // isValid ==> err == nil - expectedOutput bool // expectedCanDisable + description string + mockSettings mockSettings + isValid bool // isValid ==> err == nil + expectedOutput bool // expectedCanDisable }{ { - description: "base-ok-can-disable-backups-service-no-backups-no-backup-schedules", - listBackupsFails: false, - listBackupSchedulesFails: false, - listBackups: &serverbackup.GetBackupsListResponse{Items: &[]serverbackup.Backup{}}, - listBackupSchedules: &serverbackup.GetBackupSchedulesResponse{Items: &[]serverbackup.BackupSchedule{}}, - isValid: true, - expectedOutput: true, + description: "base-ok-can-disable-backups-service-no-backups-no-backup-schedules", + mockSettings: mockSettings{ + listBackupsFails: false, + listBackupSchedulesFails: false, + listBackupsResp: &serverbackup.GetBackupsListResponse{Items: []serverbackup.Backup{}}, + listBackupSchedulesResp: &serverbackup.GetBackupSchedulesResponse{Items: []serverbackup.BackupSchedule{}}, + }, + isValid: true, + expectedOutput: true, }, { - description: "not-ok-api-error-list-backups", - listBackupsFails: true, - listBackupSchedulesFails: false, - listBackups: &serverbackup.GetBackupsListResponse{Items: &[]serverbackup.Backup{}}, - listBackupSchedules: &serverbackup.GetBackupSchedulesResponse{Items: &[]serverbackup.BackupSchedule{}}, - isValid: false, - expectedOutput: false, + description: "not-ok-api-error-list-backups", + mockSettings: mockSettings{ + listBackupsFails: true, + listBackupSchedulesFails: false, + listBackupsResp: &serverbackup.GetBackupsListResponse{Items: []serverbackup.Backup{}}, + listBackupSchedulesResp: &serverbackup.GetBackupSchedulesResponse{Items: []serverbackup.BackupSchedule{}}, + }, + isValid: false, + expectedOutput: false, }, { - description: "not-ok-api-error-list-backup-schedules", - listBackupsFails: true, - listBackupSchedulesFails: false, - listBackups: &serverbackup.GetBackupsListResponse{Items: &[]serverbackup.Backup{}}, - listBackupSchedules: &serverbackup.GetBackupSchedulesResponse{Items: &[]serverbackup.BackupSchedule{}}, - isValid: false, - expectedOutput: false, + description: "not-ok-api-error-list-backup-schedules", + mockSettings: mockSettings{ + listBackupsFails: true, + listBackupSchedulesFails: false, + listBackupsResp: &serverbackup.GetBackupsListResponse{Items: []serverbackup.Backup{}}, + listBackupSchedulesResp: &serverbackup.GetBackupSchedulesResponse{Items: []serverbackup.BackupSchedule{}}, + }, + isValid: false, + expectedOutput: false, }, { - description: "not-ok-has-backups-cannot-disable", - listBackupsFails: false, - listBackupSchedulesFails: false, - listBackups: &serverbackup.GetBackupsListResponse{ - Items: &[]serverbackup.Backup{ - { - CreatedAt: utils.Ptr("test timestamp"), - ExpireAt: utils.Ptr("test timestamp"), - Id: utils.Ptr("5"), - LastRestoredAt: utils.Ptr("test timestamp"), - Name: utils.Ptr("test name"), - Size: utils.Ptr(int64(5)), - Status: serverbackup.BACKUPSTATUS_BACKING_UP.Ptr(), - VolumeBackups: nil, + description: "not-ok-has-backups-cannot-disable", + mockSettings: mockSettings{ + listBackupsFails: false, + listBackupSchedulesFails: false, + listBackupsResp: &serverbackup.GetBackupsListResponse{ + Items: []serverbackup.Backup{ + { + CreatedAt: "test timestamp", + ExpireAt: "test timestamp", + Id: "5", + LastRestoredAt: utils.Ptr("test timestamp"), + Name: "test name", + Size: utils.Ptr(int32(5)), + Status: serverbackup.BACKUPSTATUS_IN_PROGRESS, + VolumeBackups: nil, + }, }, }, + listBackupSchedulesResp: &serverbackup.GetBackupSchedulesResponse{Items: []serverbackup.BackupSchedule{}}, }, - listBackupSchedules: &serverbackup.GetBackupSchedulesResponse{Items: &[]serverbackup.BackupSchedule{}}, - isValid: true, - expectedOutput: false, + isValid: true, + expectedOutput: false, }, { - description: "not-ok-has-backups-schedules-cannot-disable", - listBackupsFails: false, - listBackupSchedulesFails: false, - listBackups: &serverbackup.GetBackupsListResponse{Items: &[]serverbackup.Backup{}}, - listBackupSchedules: &serverbackup.GetBackupSchedulesResponse{ - Items: &[]serverbackup.BackupSchedule{ - { - BackupProperties: nil, - Enabled: utils.Ptr(false), - Id: utils.Ptr(int64(5)), - Name: utils.Ptr("some name"), - Rrule: utils.Ptr("some rrule"), + description: "not-ok-has-backups-schedules-cannot-disable", + mockSettings: mockSettings{ + listBackupsFails: false, + listBackupSchedulesFails: false, + listBackupsResp: &serverbackup.GetBackupsListResponse{Items: []serverbackup.Backup{}}, + listBackupSchedulesResp: &serverbackup.GetBackupSchedulesResponse{ + Items: []serverbackup.BackupSchedule{ + { + BackupProperties: nil, + Enabled: false, + Id: int32(5), + Name: "some name", + Rrule: "some rrule", + }, }, }, }, @@ -120,12 +131,7 @@ func TestCanDisableBackupService(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &serverbackupClientMocked{ - listBackupsFails: tt.listBackupsFails, - listBackupSchedulesFails: tt.listBackupSchedulesFails, - listBackupsResp: tt.listBackups, - listBackupSchedulesResp: tt.listBackupSchedules, - } + client := newServerbackupClientMock(tt.mockSettings) output, err := CanDisableBackupService(context.Background(), client, testProjectId, testServerId, testRegion) diff --git a/internal/pkg/services/serverosupdate/client/client.go b/internal/pkg/services/serverosupdate/client/client.go index ec39a5477..ac2c6dba2 100644 --- a/internal/pkg/services/serverosupdate/client/client.go +++ b/internal/pkg/services/serverosupdate/client/client.go @@ -1,46 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + serverupdate "github.com/stackitcloud/stackit-sdk-go/services/serverupdate/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*serverupdate.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ServerOsUpdateCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } else { - cfgOptions = append(cfgOptions, authCfgOption) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := serverupdate.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ServerOsUpdateCustomEndpointKey), false, genericclient.CreateApiClient[*serverupdate.APIClient](serverupdate.NewAPIClient)) } diff --git a/internal/pkg/services/service-account/client/client.go b/internal/pkg/services/service-account/client/client.go index cb35d2c4d..f7150c892 100644 --- a/internal/pkg/services/service-account/client/client.go +++ b/internal/pkg/services/service-account/client/client.go @@ -1,45 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" ) func ConfigureClient(p *print.Printer, cliVersion string) (*serviceaccount.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ServiceAccountCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := serviceaccount.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ServiceAccountCustomEndpointKey), false, genericclient.CreateApiClient[*serviceaccount.APIClient](serviceaccount.NewAPIClient)) } diff --git a/internal/pkg/services/service-enablement/client/client.go b/internal/pkg/services/service-enablement/client/client.go index 66b952376..a35e8bba0 100644 --- a/internal/pkg/services/service-enablement/client/client.go +++ b/internal/pkg/services/service-enablement/client/client.go @@ -2,46 +2,14 @@ package client import ( "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*serviceenablement.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.ServiceEnablementCustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } else { - region := viper.GetString(config.RegionKey) - cfgOptions = append(cfgOptions, sdkConfig.WithRegion(region)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := serviceenablement.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.ServiceEnablementCustomEndpointKey), false, serviceenablement.NewAPIClient) } diff --git a/internal/pkg/services/service-enablement/utils/utils.go b/internal/pkg/services/service-enablement/utils/utils.go index 5f1976164..a650c692f 100644 --- a/internal/pkg/services/service-enablement/utils/utils.go +++ b/internal/pkg/services/service-enablement/utils/utils.go @@ -5,19 +5,15 @@ import ( "net/http" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" ) const ( SKEServiceId = "cloud.stackit.ske" ) -type ServiceEnablementClient interface { - GetServiceStatusRegionalExecute(ctx context.Context, region, projectId, serviceId string) (*serviceenablement.ServiceStatus, error) -} - -func ProjectEnabled(ctx context.Context, apiClient ServiceEnablementClient, projectId, region string) (bool, error) { - project, err := apiClient.GetServiceStatusRegionalExecute(ctx, region, projectId, SKEServiceId) +func ProjectEnabled(ctx context.Context, apiClient serviceenablement.DefaultAPI, projectId, region string) (bool, error) { + project, err := apiClient.GetServiceStatusRegional(ctx, region, projectId, SKEServiceId).Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped if !ok { diff --git a/internal/pkg/services/service-enablement/utils/utils_test.go b/internal/pkg/services/service-enablement/utils/utils_test.go index b898adb69..3b8b5194f 100644 --- a/internal/pkg/services/service-enablement/utils/utils_test.go +++ b/internal/pkg/services/service-enablement/utils/utils_test.go @@ -7,83 +7,96 @@ import ( "github.com/google/uuid" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement" + serviceenablement "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) +const testRegion = "eu01" + var ( testProjectId = uuid.NewString() - testRegion = "eu01" ) -type serviceEnableClientMocked struct { +type mockSettings struct { serviceDisabled bool getServiceStatusFails bool getServiceStatusResp *serviceenablement.ServiceStatus } -func (m *serviceEnableClientMocked) GetServiceStatusRegionalExecute(_ context.Context, _, _, _ string) (*serviceenablement.ServiceStatus, error) { - if m.getServiceStatusFails { - return nil, fmt.Errorf("could not get service status") - } - if m.serviceDisabled { - return nil, &oapierror.GenericOpenAPIError{StatusCode: 404} +func newServiceEnableClientMock(m mockSettings) serviceenablement.DefaultAPI { + return serviceenablement.DefaultAPIServiceMock{ + GetServiceStatusRegionalExecuteMock: utils.Ptr(func(_ serviceenablement.ApiGetServiceStatusRegionalRequest) (*serviceenablement.ServiceStatus, error) { + if m.getServiceStatusFails { + return nil, fmt.Errorf("could not get service status") + } + if m.serviceDisabled { + return nil, &oapierror.GenericOpenAPIError{StatusCode: 404} + } + return m.getServiceStatusResp, nil + }), } - return m.getServiceStatusResp, nil } func TestProjectEnabled(t *testing.T) { tests := []struct { - description string - serviceDisabled bool - getProjectFails bool - getProjectResp *serviceenablement.ServiceStatus - isValid bool - expectedOutput bool + description string + mockSettings mockSettings + isValid bool + expectedOutput bool }{ { - description: "project enabled", - getProjectResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_ENABLED.Ptr()}, + description: "project enabled", + mockSettings: mockSettings{ + getServiceStatusResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_ENABLED.Ptr()}, + }, isValid: true, expectedOutput: true, }, { - description: "project disabled (404)", - serviceDisabled: true, - isValid: true, - expectedOutput: false, + description: "project disabled (404)", + mockSettings: mockSettings{ + serviceDisabled: true, + }, + isValid: true, + expectedOutput: false, }, { - description: "project disabled 1", - getProjectResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_ENABLING.Ptr()}, + description: "project disabled 1", + mockSettings: mockSettings{ + getServiceStatusResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_ENABLING.Ptr()}, + }, isValid: true, expectedOutput: false, }, { - description: "project disabled 2", - getProjectResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_DISABLING.Ptr()}, + description: "project disabled 2", + mockSettings: mockSettings{ + getServiceStatusResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_DISABLING.Ptr()}, + }, isValid: true, expectedOutput: false, }, { - description: "project disabled 3", - getProjectResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_DISABLING.Ptr()}, + description: "project disabled 3", + mockSettings: mockSettings{ + getServiceStatusResp: &serviceenablement.ServiceStatus{State: serviceenablement.SERVICESTATUSSTATE_DISABLING.Ptr()}, + }, isValid: true, expectedOutput: false, }, { - description: "get clusters fails", - getProjectFails: true, - isValid: false, + description: "get clusters fails", + mockSettings: mockSettings{ + getServiceStatusFails: true, + }, + isValid: false, }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &serviceEnableClientMocked{ - serviceDisabled: tt.serviceDisabled, - getServiceStatusFails: tt.getProjectFails, - getServiceStatusResp: tt.getProjectResp, - } + client := newServiceEnableClientMock(tt.mockSettings) output, err := ProjectEnabled(context.Background(), client, testRegion, testProjectId) diff --git a/internal/pkg/services/sfs/client/client.go b/internal/pkg/services/sfs/client/client.go new file mode 100644 index 000000000..0a5448d54 --- /dev/null +++ b/internal/pkg/services/sfs/client/client.go @@ -0,0 +1,15 @@ +package client + +import ( + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/viper" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*sfs.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.SfsCustomEndpointKey), false, genericclient.CreateApiClient[*sfs.APIClient](sfs.NewAPIClient)) +} diff --git a/internal/pkg/services/sfs/utils/utils.go b/internal/pkg/services/sfs/utils/utils.go new file mode 100644 index 000000000..c78e0d7e3 --- /dev/null +++ b/internal/pkg/services/sfs/utils/utils.go @@ -0,0 +1,41 @@ +package utils + +import ( + "context" + "fmt" + + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" +) + +func GetShareName(ctx context.Context, client sfs.DefaultAPI, projectId, region, resourcePoolId, shareId string) (string, error) { + resp, err := client.GetShare(ctx, projectId, region, resourcePoolId, shareId).Execute() + if err != nil { + return "", fmt.Errorf("get share: %w", err) + } + if resp != nil && resp.Share != nil && resp.Share.Name != nil { + return *resp.Share.Name, nil + } + return "", nil +} + +func GetExportPolicyName(ctx context.Context, apiClient sfs.DefaultAPI, projectId, region, policyId string) (string, error) { + resp, err := apiClient.GetShareExportPolicy(ctx, projectId, region, policyId).Execute() + if err != nil { + return "", fmt.Errorf("get share export policy: %w", err) + } + if resp != nil && resp.ShareExportPolicy != nil && resp.ShareExportPolicy.Name != nil { + return *resp.ShareExportPolicy.Name, nil + } + return "", nil +} + +func GetResourcePoolName(ctx context.Context, client sfs.DefaultAPI, projectId, region, resourcePoolId string) (string, error) { + resp, err := client.GetResourcePool(ctx, projectId, region, resourcePoolId).Execute() + if err != nil { + return "", fmt.Errorf("get resource pool: %w", err) + } + if resp != nil && resp.ResourcePool != nil && resp.ResourcePool.Name != nil { + return *resp.ResourcePool.Name, nil + } + return "", nil +} diff --git a/internal/pkg/services/sfs/utils/utils_test.go b/internal/pkg/services/sfs/utils/utils_test.go new file mode 100644 index 000000000..5c4b6d1f4 --- /dev/null +++ b/internal/pkg/services/sfs/utils/utils_test.go @@ -0,0 +1,213 @@ +package utils + +import ( + "context" + "fmt" + "testing" + + "github.com/google/uuid" + sfs "github.com/stackitcloud/stackit-sdk-go/services/sfs/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" +) + +const ( + testShareName = "share-name" + testResourcePoolName = "resource-pool-name" + testExportPolicyName = "export-policy-name" + testRegion = "eu01" +) + +var ( + testPolicyId = uuid.NewString() + testProjectId = uuid.NewString() +) + +type mockSettings struct { + getShareFails bool + getShareResp *sfs.GetShareResponse + + getResourcePoolFails bool + getResourcePoolResp *sfs.GetResourcePoolResponse + + getExportPolicyFails bool + getExportPolicyResp *sfs.GetShareExportPolicyResponse +} + +func newAPIMock(settings *mockSettings) sfs.DefaultAPI { + return &sfs.DefaultAPIServiceMock{ + GetShareExecuteMock: utils.Ptr(func(_ sfs.ApiGetShareRequest) (*sfs.GetShareResponse, error) { + if settings.getShareFails { + return nil, fmt.Errorf("could not get share details") + } + + return settings.getShareResp, nil + }), + GetShareExportPolicyExecuteMock: utils.Ptr(func(_ sfs.ApiGetShareExportPolicyRequest) (*sfs.GetShareExportPolicyResponse, error) { + if settings.getExportPolicyFails { + return nil, fmt.Errorf("could not get export policy details") + } + + return settings.getExportPolicyResp, nil + }), + GetResourcePoolExecuteMock: utils.Ptr(func(_ sfs.ApiGetResourcePoolRequest) (*sfs.GetResourcePoolResponse, error) { + if settings.getResourcePoolFails { + return nil, fmt.Errorf("could not get resource pool details") + } + + return settings.getResourcePoolResp, nil + }), + } +} + +func TestGetExportPolicyName(t *testing.T) { + tests := []struct { + description string + getExportPolicyResp *sfs.GetShareExportPolicyResponse + getExportPolicyFails bool + isValid bool + expectedOutput string + }{ + { + description: "base", + getExportPolicyResp: &sfs.GetShareExportPolicyResponse{ + ShareExportPolicy: &sfs.ShareExportPolicy{ + Name: utils.Ptr(testExportPolicyName), + }, + }, + isValid: true, + expectedOutput: testExportPolicyName, + }, + { + description: "get export policy fails", + getExportPolicyFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := newAPIMock(&mockSettings{ + getExportPolicyFails: tt.getExportPolicyFails, + getExportPolicyResp: tt.getExportPolicyResp, + }) + + output, err := GetExportPolicyName(context.Background(), client, testProjectId, testRegion, testPolicyId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} + +func TestGetShareName(t *testing.T) { + tests := []struct { + description string + getShareResp *sfs.GetShareResponse + getShareFails bool + isValid bool + expectedOutput string + }{ + { + description: "base", + getShareResp: &sfs.GetShareResponse{ + Share: &sfs.Share{ + Name: utils.Ptr(testShareName), + }, + }, + isValid: true, + expectedOutput: testShareName, + }, + { + description: "get share fails", + getShareFails: true, + isValid: false, + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := newAPIMock(&mockSettings{ + getShareFails: tt.getShareFails, + getShareResp: tt.getShareResp, + }) + + output, err := GetShareName(context.Background(), client, testProjectId, testRegion, "", "") + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} + +func TestGetResourcePoolName(t *testing.T) { + tests := []struct { + description string + getResourcePoolResp *sfs.GetResourcePoolResponse + getResourcePoolFails bool + isValid bool + expectedOutput string + }{ + { + description: "base", + getResourcePoolResp: &sfs.GetResourcePoolResponse{ + ResourcePool: &sfs.ResourcePool{ + Name: utils.Ptr(testResourcePoolName), + }, + }, + isValid: true, + expectedOutput: testResourcePoolName, + }, + { + description: "get resource pool fails", + getResourcePoolFails: true, + isValid: false, + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := newAPIMock(&mockSettings{ + getResourcePoolResp: tt.getResourcePoolResp, + getResourcePoolFails: tt.getResourcePoolFails, + }) + + output, err := GetResourcePoolName(context.Background(), client, testProjectId, testRegion, "") + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} diff --git a/internal/pkg/services/ske/client/client.go b/internal/pkg/services/ske/client/client.go index 1495ba7bf..1fba24bc6 100644 --- a/internal/pkg/services/ske/client/client.go +++ b/internal/pkg/services/ske/client/client.go @@ -1,46 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*ske.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.SKECustomEndpointKey) - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } else { - cfgOptions = append(cfgOptions, authCfgOption) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := ske.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.SKECustomEndpointKey), false, ske.NewAPIClient) } diff --git a/internal/pkg/services/ske/utils/utils.go b/internal/pkg/services/ske/utils/utils.go index 605ca4158..11b4b3930 100644 --- a/internal/pkg/services/ske/utils/utils.go +++ b/internal/pkg/services/ske/utils/utils.go @@ -6,23 +6,21 @@ import ( "maps" "os" "path/filepath" + "regexp" "strconv" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "k8s.io/client-go/tools/clientcmd" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" "golang.org/x/mod/semver" ) const ( - defaultNodepoolAvailabilityZone = "eu01-3" - defaultNodepoolCRI = ske.CRINAME_CONTAINERD - defaultNodepoolMachineType = "b1.2" + defaultNodepoolCRI = "containerd" defaultNodepoolMachineImageName = "flatcar" - defaultNodepoolMaxSurge = 1 defaultNodepoolMaxUnavailable = 0 - defaultNodepoolMaximum = 2 defaultNodepoolMinimum = 1 defaultNodepoolName = "pool-default" defaultNodepoolVolumeType = "storage_premium_perf2" @@ -31,17 +29,12 @@ const ( supportedState = "supported" ) -type SKEClient interface { - ListClustersExecute(ctx context.Context, projectId, region string) (*ske.ListClustersResponse, error) - ListProviderOptionsExecute(ctx context.Context, region string) (*ske.ProviderOptions, error) -} - -func ClusterExists(ctx context.Context, apiClient SKEClient, projectId, region, clusterName string) (bool, error) { - clusters, err := apiClient.ListClustersExecute(ctx, projectId, region) +func ClusterExists(ctx context.Context, apiClient ske.DefaultAPI, projectId, region, clusterName string) (bool, error) { + clusters, err := apiClient.ListClusters(ctx, projectId, region).Execute() if err != nil { return false, fmt.Errorf("list SKE clusters: %w", err) } - for _, cl := range *clusters.Items { + for _, cl := range clusters.Items { if cl.Name != nil && *cl.Name == clusterName { return true, nil } @@ -49,8 +42,8 @@ func ClusterExists(ctx context.Context, apiClient SKEClient, projectId, region, return false, nil } -func GetDefaultPayload(ctx context.Context, apiClient SKEClient, region string) (*ske.CreateOrUpdateClusterPayload, error) { - resp, err := apiClient.ListProviderOptionsExecute(ctx, region) +func GetDefaultPayload(ctx context.Context, apiClient ske.DefaultAPI, region string) (*ske.CreateOrUpdateClusterPayload, error) { + resp, err := apiClient.ListProviderOptions(ctx, region).Execute() if err != nil { return nil, fmt.Errorf("get SKE provider options: %w", err) } @@ -67,70 +60,91 @@ func GetDefaultPayload(ctx context.Context, apiClient SKEClient, region string) payload := &ske.CreateOrUpdateClusterPayload{ Extensions: &ske.Extension{ Acl: &ske.ACL{ - AllowedCidrs: &[]string{}, - Enabled: utils.Ptr(false), + AllowedCidrs: []string{}, + Enabled: false, }, }, Kubernetes: payloadKubernetes, - Nodepools: &[]ske.Nodepool{ + Nodepools: []ske.Nodepool{ *payloadNodepool, }, } return payload, nil } -func getDefaultPayloadKubernetes(resp *ske.ProviderOptions) (*ske.Kubernetes, error) { - output := &ske.Kubernetes{} +func getDefaultPayloadKubernetes(resp *ske.ProviderOptions) (ske.Kubernetes, error) { + output := ske.Kubernetes{} if resp.KubernetesVersions == nil { - return nil, fmt.Errorf("no supported Kubernetes version found") + return ske.Kubernetes{}, fmt.Errorf("no supported Kubernetes version found") } foundKubernetesVersion := false - versions := *resp.KubernetesVersions + versions := resp.KubernetesVersions for i := range versions { version := versions[i] if *version.State != supportedState { continue } - if output.Version != nil { - oldSemVer := fmt.Sprintf("v%s", *output.Version) - newSemVer := fmt.Sprintf("v%s", *version.Version) + if output.Version != "" { + oldSemVer := fmt.Sprintf("v%s", output.Version) + newSemVer := fmt.Sprintf("v%s", version.GetVersion()) if semver.Compare(newSemVer, oldSemVer) != 1 { continue } } foundKubernetesVersion = true - output.Version = version.Version + output.Version = version.GetVersion() } if !foundKubernetesVersion { - return nil, fmt.Errorf("no supported Kubernetes version found") + return ske.Kubernetes{}, fmt.Errorf("no supported Kubernetes version found") } return output, nil } func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) { + if len(resp.AvailabilityZones) == 0 { + return nil, fmt.Errorf("no availability zones found") + } + var availabilityZones []string + for i := range resp.AvailabilityZones { + azName := resp.AvailabilityZones[i].GetName() + // don't include availability zones like eu01-m, eu02-m, not all flavors are available there + if !regexp.MustCompile(`\w{2}\d{2}-m`).MatchString(azName) { + availabilityZones = append(availabilityZones, azName) + } + } + + if len(resp.MachineTypes) == 0 { + return nil, fmt.Errorf("no machine types found") + } + azLen := len(availabilityZones) + if azLen > 1000 { + // check against a very large number to avoid gosec warning + return nil, fmt.Errorf("invalid number of availability zones") + } + machineType := resp.MachineTypes[0].GetName() + output := &ske.Nodepool{ - AvailabilityZones: &[]string{ - defaultNodepoolAvailabilityZone, - }, + AvailabilityZones: availabilityZones, Cri: &ske.CRI{ Name: utils.Ptr(defaultNodepoolCRI), }, - Machine: &ske.Machine{ - Type: utils.Ptr(defaultNodepoolMachineType), - Image: &ske.Image{ - Name: utils.Ptr(defaultNodepoolMachineImageName), + Machine: ske.Machine{ + Type: machineType, + Image: ske.Image{ + Name: defaultNodepoolMachineImageName, }, }, - MaxSurge: utils.Ptr(int64(defaultNodepoolMaxSurge)), - MaxUnavailable: utils.Ptr(int64(defaultNodepoolMaxUnavailable)), - Maximum: utils.Ptr(int64(defaultNodepoolMaximum)), - Minimum: utils.Ptr(int64(defaultNodepoolMinimum)), - Name: utils.Ptr(defaultNodepoolName), - Volume: &ske.Volume{ + // there must be as many nodes as availability zones are given + MaxSurge: utils.Ptr(int32(azLen)), + MaxUnavailable: utils.Ptr(int32(defaultNodepoolMaxUnavailable)), + Maximum: int32(azLen), + Minimum: int32(defaultNodepoolMinimum), + Name: defaultNodepoolName, + Volume: ske.Volume{ Type: utils.Ptr(defaultNodepoolVolumeType), - Size: utils.Ptr(int64(defaultNodepoolVolumeSize)), + Size: int32(defaultNodepoolVolumeSize), }, } @@ -139,7 +153,7 @@ func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) return nil, fmt.Errorf("no supported image versions found") } foundImageVersion := false - images := *resp.MachineImages + images := resp.MachineImages for i := range images { image := images[i] if *image.Name != defaultNodepoolMachineImageName { @@ -148,7 +162,7 @@ func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) if image.Versions == nil { continue } - versions := *image.Versions + versions := image.Versions for j := range versions { version := versions[j] if *version.State != supportedState { @@ -156,12 +170,12 @@ func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) } // Check if default CRI is supported - if version.Cri == nil || len(*version.Cri) == 0 { + if len(version.Cri) == 0 { continue } criSupported := false - for k := range *version.Cri { - cri := (*version.Cri)[k] + for k := range version.Cri { + cri := version.Cri[k] if *cri.Name == defaultNodepoolCRI { criSupported = true break @@ -171,8 +185,8 @@ func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) continue } - if output.Machine.Image.Version != nil { - oldSemVer := fmt.Sprintf("v%s", *output.Machine.Image.Version) + if output.Machine.Image.Version != "" { + oldSemVer := fmt.Sprintf("v%s", output.Machine.Image.Version) newSemVer := fmt.Sprintf("v%s", *version.Version) if semver.Compare(newSemVer, oldSemVer) != 1 { continue @@ -180,7 +194,7 @@ func getDefaultPayloadNodepool(resp *ske.ProviderOptions) (*ske.Nodepool, error) } foundImageVersion = true - output.Machine.Image.Version = version.Version + output.Machine.Image.Version = version.GetVersion() } } if !foundImageVersion { diff --git a/internal/pkg/services/ske/utils/utils_test.go b/internal/pkg/services/ske/utils/utils_test.go index 917d590ae..23f8adbac 100644 --- a/internal/pkg/services/ske/utils/utils_test.go +++ b/internal/pkg/services/ske/utils/utils_test.go @@ -7,12 +7,13 @@ import ( "path/filepath" "testing" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "k8s.io/client-go/tools/clientcmd" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/ske" + ske "github.com/stackitcloud/stackit-sdk-go/services/ske/v2api" ) var ( @@ -63,29 +64,8 @@ users: ` ) -type skeClientMocked struct { - listClustersFails bool - listClustersResp *ske.ListClustersResponse - listProviderOptionsFails bool - listProviderOptionsResp *ske.ProviderOptions -} - const testRegion = "eu01" -func (m *skeClientMocked) ListClustersExecute(_ context.Context, _, _ string) (*ske.ListClustersResponse, error) { - if m.listClustersFails { - return nil, fmt.Errorf("could not list clusters") - } - return m.listClustersResp, nil -} - -func (m *skeClientMocked) ListProviderOptionsExecute(_ context.Context, _ string) (*ske.ProviderOptions, error) { - if m.listProviderOptionsFails { - return nil, fmt.Errorf("could not list provider options") - } - return m.listProviderOptionsResp, nil -} - func TestClusterExists(t *testing.T) { tests := []struct { description string @@ -96,19 +76,19 @@ func TestClusterExists(t *testing.T) { }{ { description: "cluster exists", - getClustersResp: &ske.ListClustersResponse{Items: &[]ske.Cluster{{Name: utils.Ptr(testClusterName)}}}, + getClustersResp: &ske.ListClustersResponse{Items: []ske.Cluster{{Name: utils.Ptr(testClusterName)}}}, isValid: true, expectedExists: true, }, { description: "cluster exists 2", - getClustersResp: &ske.ListClustersResponse{Items: &[]ske.Cluster{{Name: utils.Ptr("some-cluster")}, {Name: utils.Ptr("some-other-cluster")}, {Name: utils.Ptr(testClusterName)}}}, + getClustersResp: &ske.ListClustersResponse{Items: []ske.Cluster{{Name: utils.Ptr("some-cluster")}, {Name: utils.Ptr("some-other-cluster")}, {Name: utils.Ptr(testClusterName)}}}, isValid: true, expectedExists: true, }, { description: "cluster does not exist", - getClustersResp: &ske.ListClustersResponse{Items: &[]ske.Cluster{{Name: utils.Ptr("some-cluster")}, {Name: utils.Ptr("some-other-cluster")}}}, + getClustersResp: &ske.ListClustersResponse{Items: []ske.Cluster{{Name: utils.Ptr("some-cluster")}, {Name: utils.Ptr("some-other-cluster")}}}, isValid: true, expectedExists: false, }, @@ -121,9 +101,13 @@ func TestClusterExists(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &skeClientMocked{ - listClustersFails: tt.getClustersFails, - listClustersResp: tt.getClustersResp, + client := &ske.DefaultAPIServiceMock{ + ListClustersExecuteMock: utils.Ptr(func(_ ske.ApiListClustersRequest) (*ske.ListClustersResponse, error) { + if tt.getClustersFails { + return nil, fmt.Errorf("could not list clusters") + } + return tt.getClustersResp, nil + }), } exists, err := ClusterExists(context.Background(), client, testProjectId, testRegion, testClusterName) @@ -146,7 +130,18 @@ func TestClusterExists(t *testing.T) { func fixtureProviderOptions(mods ...func(*ske.ProviderOptions)) *ske.ProviderOptions { providerOptions := &ske.ProviderOptions{ - KubernetesVersions: &[]ske.KubernetesVersion{ + AvailabilityZones: []ske.AvailabilityZone{ + {Name: utils.Ptr("eu01-m")}, + {Name: utils.Ptr("eu01-1")}, + {Name: utils.Ptr("eu01-2")}, + {Name: utils.Ptr("eu01-3")}, + }, + MachineTypes: []ske.MachineType{ + { + Name: utils.Ptr("b1.2"), + }, + }, + KubernetesVersions: []ske.KubernetesVersion{ { State: utils.Ptr("supported"), Version: utils.Ptr("1.2.3"), @@ -160,31 +155,31 @@ func fixtureProviderOptions(mods ...func(*ske.ProviderOptions)) *ske.ProviderOpt Version: utils.Ptr("4.4.4"), }, }, - MachineImages: &[]ske.MachineImage{ + MachineImages: []ske.MachineImage{ { Name: utils.Ptr("flatcar"), - Versions: &[]ske.MachineImageVersion{ + Versions: []ske.MachineImageVersion{ { State: utils.Ptr("supported"), Version: utils.Ptr("1.2.3"), - Cri: &[]ske.CRI{ + Cri: []ske.CRI{ { - Name: ske.CRINAME_DOCKER.Ptr(), + Name: utils.Ptr("docker"), }, { - Name: ske.CRINAME_CONTAINERD.Ptr(), + Name: utils.Ptr("containerd"), }, }, }, { State: utils.Ptr("supported"), Version: utils.Ptr("3.2.1"), - Cri: &[]ske.CRI{ + Cri: []ske.CRI{ { - Name: ske.CRINAME_DOCKER.Ptr(), + Name: utils.Ptr("docker"), }, { - Name: ske.CRINAME_CONTAINERD.Ptr(), + Name: utils.Ptr("containerd"), }, }, }, @@ -192,13 +187,13 @@ func fixtureProviderOptions(mods ...func(*ske.ProviderOptions)) *ske.ProviderOpt }, { Name: utils.Ptr("not-flatcar"), - Versions: &[]ske.MachineImageVersion{ + Versions: []ske.MachineImageVersion{ { State: utils.Ptr("supported"), Version: utils.Ptr("4.4.4"), - Cri: &[]ske.CRI{ + Cri: []ske.CRI{ { - Name: ske.CRINAME_CONTAINERD.Ptr(), + Name: utils.Ptr("containerd"), }, }, }, @@ -206,7 +201,7 @@ func fixtureProviderOptions(mods ...func(*ske.ProviderOptions)) *ske.ProviderOpt }, { Name: utils.Ptr("flatcar"), - Versions: &[]ske.MachineImageVersion{ + Versions: []ske.MachineImageVersion{ { State: utils.Ptr("supported"), Version: utils.Ptr("4.4.4"), @@ -215,13 +210,13 @@ func fixtureProviderOptions(mods ...func(*ske.ProviderOptions)) *ske.ProviderOpt }, { Name: utils.Ptr("flatcar"), - Versions: &[]ske.MachineImageVersion{ + Versions: []ske.MachineImageVersion{ { State: utils.Ptr("not-supported"), Version: utils.Ptr("4.4.4"), - Cri: &[]ske.CRI{ + Cri: []ske.CRI{ { - Name: ske.CRINAME_CONTAINERD.Ptr(), + Name: utils.Ptr("containerd"), }, }, }, @@ -229,13 +224,13 @@ func fixtureProviderOptions(mods ...func(*ske.ProviderOptions)) *ske.ProviderOpt }, { Name: utils.Ptr("flatcar"), - Versions: &[]ske.MachineImageVersion{ + Versions: []ske.MachineImageVersion{ { State: utils.Ptr("supported"), Version: utils.Ptr("4.4.4"), - Cri: &[]ske.CRI{ + Cri: []ske.CRI{ { - Name: ske.CRINAME_DOCKER.Ptr(), + Name: utils.Ptr("docker"), }, }, }, @@ -253,36 +248,38 @@ func fixtureGetDefaultPayload(mods ...func(*ske.CreateOrUpdateClusterPayload)) * payload := &ske.CreateOrUpdateClusterPayload{ Extensions: &ske.Extension{ Acl: &ske.ACL{ - AllowedCidrs: &[]string{}, - Enabled: utils.Ptr(false), + AllowedCidrs: []string{}, + Enabled: false, }, }, - Kubernetes: &ske.Kubernetes{ - Version: utils.Ptr("3.2.1"), + Kubernetes: ske.Kubernetes{ + Version: "3.2.1", }, - Nodepools: &[]ske.Nodepool{ + Nodepools: []ske.Nodepool{ { - AvailabilityZones: &[]string{ + AvailabilityZones: []string{ + "eu01-1", + "eu01-2", "eu01-3", }, Cri: &ske.CRI{ - Name: ske.CRINAME_CONTAINERD.Ptr(), + Name: utils.Ptr("containerd"), }, - Machine: &ske.Machine{ - Type: utils.Ptr("b1.2"), - Image: &ske.Image{ - Version: utils.Ptr("3.2.1"), - Name: utils.Ptr("flatcar"), + Machine: ske.Machine{ + Type: "b1.2", + Image: ske.Image{ + Version: "3.2.1", + Name: "flatcar", }, }, - MaxSurge: utils.Ptr(int64(1)), - MaxUnavailable: utils.Ptr(int64(0)), - Maximum: utils.Ptr(int64(2)), - Minimum: utils.Ptr(int64(1)), - Name: utils.Ptr("pool-default"), - Volume: &ske.Volume{ + MaxSurge: utils.Ptr(int32(3)), + MaxUnavailable: utils.Ptr(int32(0)), + Maximum: int32(3), + Minimum: int32(1), + Name: "pool-default", + Volume: ske.Volume{ Type: utils.Ptr("storage_premium_perf2"), - Size: utils.Ptr(int64(50)), + Size: int32(50), }, }, }, @@ -312,6 +309,34 @@ func TestGetDefaultPayload(t *testing.T) { listProviderOptionsFails: true, isValid: false, }, + { + description: "availability zones nil", + listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { + po.AvailabilityZones = nil + }), + isValid: false, + }, + { + description: "no availability zones", + listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { + po.AvailabilityZones = []ske.AvailabilityZone{} + }), + isValid: false, + }, + { + description: "machine types nil", + listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { + po.MachineTypes = nil + }), + isValid: false, + }, + { + description: "no machine types", + listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { + po.MachineTypes = []ske.MachineType{} + }), + isValid: false, + }, { description: "no Kubernetes versions 1", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { @@ -322,14 +347,14 @@ func TestGetDefaultPayload(t *testing.T) { { description: "no Kubernetes versions 2", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { - po.KubernetesVersions = &[]ske.KubernetesVersion{} + po.KubernetesVersions = []ske.KubernetesVersion{} }), isValid: false, }, { description: "no supported Kubernetes versions", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { - po.KubernetesVersions = &[]ske.KubernetesVersion{ + po.KubernetesVersions = []ske.KubernetesVersion{ { State: utils.Ptr("not-supported"), Version: utils.Ptr("1.2.3"), @@ -341,7 +366,7 @@ func TestGetDefaultPayload(t *testing.T) { { description: "no machine images 1", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { - po.MachineImages = &[]ske.MachineImage{} + po.MachineImages = []ske.MachineImage{} }), isValid: false, }, @@ -355,7 +380,7 @@ func TestGetDefaultPayload(t *testing.T) { { description: "no machine image versions 1", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { - po.MachineImages = &[]ske.MachineImage{ + po.MachineImages = []ske.MachineImage{ { Name: utils.Ptr("image-1"), Versions: nil, @@ -367,10 +392,10 @@ func TestGetDefaultPayload(t *testing.T) { { description: "no machine image versions 2", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { - po.MachineImages = &[]ske.MachineImage{ + po.MachineImages = []ske.MachineImage{ { Name: utils.Ptr("image-1"), - Versions: &[]ske.MachineImageVersion{}, + Versions: []ske.MachineImageVersion{}, }, } }), @@ -379,10 +404,10 @@ func TestGetDefaultPayload(t *testing.T) { { description: "no supported machine image versions", listProviderOptionsResp: fixtureProviderOptions(func(po *ske.ProviderOptions) { - po.MachineImages = &[]ske.MachineImage{ + po.MachineImages = []ske.MachineImage{ { Name: utils.Ptr("image-1"), - Versions: &[]ske.MachineImageVersion{ + Versions: []ske.MachineImageVersion{ { State: utils.Ptr("not-supported"), Version: utils.Ptr("1.2.3"), @@ -397,9 +422,13 @@ func TestGetDefaultPayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &skeClientMocked{ - listProviderOptionsFails: tt.listProviderOptionsFails, - listProviderOptionsResp: tt.listProviderOptionsResp, + client := &ske.DefaultAPIServiceMock{ + ListProviderOptionsExecuteMock: utils.Ptr(func(_ ske.ApiListProviderOptionsRequest) (*ske.ProviderOptions, error) { + if tt.listProviderOptionsFails { + return nil, fmt.Errorf("could not list provider options") + } + return tt.listProviderOptionsResp, nil + }), } output, err := GetDefaultPayload(context.Background(), client, testRegion) @@ -683,6 +712,8 @@ func TestGetDefaultKubeconfigPath(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { + // prevent test from failing if user has set the environment variable + t.Setenv("KUBECONFIG", "") output, err := GetDefaultKubeconfigPath() if err != nil { diff --git a/internal/pkg/services/sqlserverflex/client/client.go b/internal/pkg/services/sqlserverflex/client/client.go index 30e937f59..693108c57 100644 --- a/internal/pkg/services/sqlserverflex/client/client.go +++ b/internal/pkg/services/sqlserverflex/client/client.go @@ -1,45 +1,14 @@ package client import ( - "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" "github.com/stackitcloud/stackit-cli/internal/pkg/print" - "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/spf13/viper" - sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) func ConfigureClient(p *print.Printer, cliVersion string) (*sqlserverflex.APIClient, error) { - authCfgOption, err := auth.AuthenticationConfig(p, auth.AuthorizeUser) - if err != nil { - p.Debug(print.ErrorLevel, "configure authentication: %v", err) - return nil, &errors.AuthError{} - } - cfgOptions := []sdkConfig.ConfigurationOption{ - utils.UserAgentConfigOption(cliVersion), - authCfgOption, - } - - customEndpoint := viper.GetString(config.SQLServerFlexCustomEndpointKey) - - if customEndpoint != "" { - cfgOptions = append(cfgOptions, sdkConfig.WithEndpoint(customEndpoint)) - } - - if p.IsVerbosityDebug() { - cfgOptions = append(cfgOptions, - sdkConfig.WithMiddleware(print.RequestResponseCapturer(p, nil)), - ) - } - - apiClient, err := sqlserverflex.NewAPIClient(cfgOptions...) - if err != nil { - p.Debug(print.ErrorLevel, "create new API client: %v", err) - return nil, &errors.AuthError{} - } - - return apiClient, nil + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.SQLServerFlexCustomEndpointKey), false, genericclient.CreateApiClient[*sqlserverflex.APIClient](sqlserverflex.NewAPIClient)) } diff --git a/internal/pkg/services/sqlserverflex/utils/utils.go b/internal/pkg/services/sqlserverflex/utils/utils.go index 768507175..fe8e7915f 100644 --- a/internal/pkg/services/sqlserverflex/utils/utils.go +++ b/internal/pkg/services/sqlserverflex/utils/utils.go @@ -7,7 +7,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/errors" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) const ( @@ -16,21 +16,21 @@ const ( // enforce implementation of interfaces var ( - _ SQLServerFlexClient = &sqlserverflex.APIClient{} + _ SQLServerFlexClient = sqlserverflex.APIClient{}.DefaultAPI ) type SQLServerFlexClient interface { - ListVersionsExecute(ctx context.Context, projectId string, region string) (*sqlserverflex.ListVersionsResponse, error) - GetInstanceExecute(ctx context.Context, projectId, instanceId string, region string) (*sqlserverflex.GetInstanceResponse, error) - GetUserExecute(ctx context.Context, projectId, instanceId, userId string, region string) (*sqlserverflex.GetUserResponse, error) + ListVersions(ctx context.Context, projectId string, region string) sqlserverflex.ApiListVersionsRequest + GetInstance(ctx context.Context, projectId, instanceId string, region string) sqlserverflex.ApiGetInstanceRequest + GetUser(ctx context.Context, projectId, instanceId, userId string, region string) sqlserverflex.ApiGetUserRequest } -func ValidateFlavorId(flavorId string, flavors *[]sqlserverflex.InstanceFlavorEntry) error { +func ValidateFlavorId(flavorId string, flavors []sqlserverflex.InstanceFlavorEntry) error { if flavors == nil { return fmt.Errorf("nil flavors") } - for _, f := range *flavors { + for _, f := range flavors { if f.Id != nil && strings.EqualFold(*f.Id, flavorId) { return nil } @@ -57,7 +57,7 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *sqlserv return nil } - for _, sc := range *storages.StorageClasses { + for _, sc := range storages.StorageClasses { if strings.EqualFold(*storageClass, sc) { return nil } @@ -69,13 +69,13 @@ func ValidateStorage(storageClass *string, storageSize *int64, storages *sqlserv } } -func LoadFlavorId(cpu, ram int64, flavors *[]sqlserverflex.InstanceFlavorEntry) (*string, error) { +func LoadFlavorId(cpu, ram int32, flavors []sqlserverflex.InstanceFlavorEntry) (*string, error) { if flavors == nil { return nil, fmt.Errorf("nil flavors") } availableFlavors := "" - for _, f := range *flavors { + for _, f := range flavors { if f.Id == nil || f.Cpu == nil || f.Memory == nil { continue } @@ -91,7 +91,7 @@ func LoadFlavorId(cpu, ram int64, flavors *[]sqlserverflex.InstanceFlavorEntry) } func GetInstanceName(ctx context.Context, apiClient SQLServerFlexClient, projectId, instanceId, region string) (string, error) { - resp, err := apiClient.GetInstanceExecute(ctx, projectId, instanceId, region) + resp, err := apiClient.GetInstance(ctx, projectId, instanceId, region).Execute() if err != nil { return "", fmt.Errorf("get SQLServer Flex instance: %w", err) } @@ -99,7 +99,7 @@ func GetInstanceName(ctx context.Context, apiClient SQLServerFlexClient, project } func GetUserName(ctx context.Context, apiClient SQLServerFlexClient, projectId, instanceId, userId, region string) (string, error) { - resp, err := apiClient.GetUserExecute(ctx, projectId, instanceId, userId, region) + resp, err := apiClient.GetUser(ctx, projectId, instanceId, userId, region).Execute() if err != nil { return "", fmt.Errorf("get SQLServer Flex user: %w", err) } diff --git a/internal/pkg/services/sqlserverflex/utils/utils_test.go b/internal/pkg/services/sqlserverflex/utils/utils_test.go index 00c73e376..fc57748eb 100644 --- a/internal/pkg/services/sqlserverflex/utils/utils_test.go +++ b/internal/pkg/services/sqlserverflex/utils/utils_test.go @@ -9,16 +9,13 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/uuid" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v2api" ) var ( testProjectId = uuid.NewString() testInstanceId = uuid.NewString() testUserId = uuid.NewString() - - // enforce implementation of interfaces - _ SQLServerFlexClient = &sqlServerFlexClientMocked{} ) const ( @@ -27,7 +24,7 @@ const ( testRegion = "eu01" ) -type sqlServerFlexClientMocked struct { +type mockSettings struct { listVersionsFails bool listVersionsResp *sqlserverflex.ListVersionsResponse getInstanceFails bool @@ -38,32 +35,33 @@ type sqlServerFlexClientMocked struct { listRestoreJobsResp *sqlserverflex.ListRestoreJobsResponse } -func (m *sqlServerFlexClientMocked) ListVersionsExecute(_ context.Context, _, _ string) (*sqlserverflex.ListVersionsResponse, error) { - if m.listVersionsFails { - return nil, fmt.Errorf("could not list versions") - } - return m.listVersionsResp, nil -} - -func (m *sqlServerFlexClientMocked) ListRestoreJobsExecute(_ context.Context, _, _, _ string) (*sqlserverflex.ListRestoreJobsResponse, error) { - if m.listRestoreJobsFails { - return nil, fmt.Errorf("could not list versions") - } - return m.listRestoreJobsResp, nil -} - -func (m *sqlServerFlexClientMocked) GetInstanceExecute(_ context.Context, _, _, _ string) (*sqlserverflex.GetInstanceResponse, error) { - if m.getInstanceFails { - return nil, fmt.Errorf("could not get instance") - } - return m.getInstanceResp, nil -} - -func (m *sqlServerFlexClientMocked) GetUserExecute(_ context.Context, _, _, _, _ string) (*sqlserverflex.GetUserResponse, error) { - if m.getUserFails { - return nil, fmt.Errorf("could not get user") +func newApiMock(s *mockSettings) sqlserverflex.DefaultAPI { + return &sqlserverflex.DefaultAPIServiceMock{ + ListVersionsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListVersionsRequest) (*sqlserverflex.ListVersionsResponse, error) { + if s.listVersionsFails { + return nil, fmt.Errorf("could not list versions") + } + return s.listVersionsResp, nil + }), + GetInstanceExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiGetInstanceRequest) (*sqlserverflex.GetInstanceResponse, error) { + if s.getInstanceFails { + return nil, fmt.Errorf("could not get instance") + } + return s.getInstanceResp, nil + }), + GetUserExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiGetUserRequest) (*sqlserverflex.GetUserResponse, error) { + if s.getUserFails { + return nil, fmt.Errorf("could not get user") + } + return s.getUserResp, nil + }), + ListRestoreJobsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListRestoreJobsRequest) (*sqlserverflex.ListRestoreJobsResponse, error) { + if s.listRestoreJobsFails { + return nil, fmt.Errorf("could not list versions") + } + return s.listRestoreJobsResp, nil + }), } - return m.getUserResp, nil } func TestValidateStorage(t *testing.T) { @@ -79,7 +77,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(10)), storages: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -99,7 +97,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(1)), storages: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -112,7 +110,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(200)), storages: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -125,7 +123,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(5)), storages: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -138,7 +136,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(20)), storages: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "foo"}, + StorageClasses: []string{"bar-1", "bar-2", "foo"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -151,7 +149,7 @@ func TestValidateStorage(t *testing.T) { storageClass: utils.Ptr("foo"), storageSize: utils.Ptr(int64(10)), storages: &sqlserverflex.ListStoragesResponse{ - StorageClasses: &[]string{"bar-1", "bar-2", "bar-3"}, + StorageClasses: []string{"bar-1", "bar-2", "bar-3"}, StorageRange: &sqlserverflex.StorageRange{ Min: utils.Ptr(int64(5)), Max: utils.Ptr(int64(20)), @@ -178,13 +176,13 @@ func TestValidateFlavorId(t *testing.T) { tests := []struct { description string flavorId string - flavors *[]sqlserverflex.InstanceFlavorEntry + flavors []sqlserverflex.InstanceFlavorEntry isValid bool }{ { description: "base", flavorId: "foo", - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ {Id: utils.Ptr("bar-1")}, {Id: utils.Ptr("bar-2")}, {Id: utils.Ptr("foo")}, @@ -200,13 +198,13 @@ func TestValidateFlavorId(t *testing.T) { { description: "no flavors", flavorId: "foo", - flavors: &[]sqlserverflex.InstanceFlavorEntry{}, + flavors: []sqlserverflex.InstanceFlavorEntry{}, isValid: false, }, { description: "nil flavor id", flavorId: "foo", - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ {Id: utils.Ptr("bar-1")}, {Id: nil}, {Id: utils.Ptr("foo")}, @@ -216,7 +214,7 @@ func TestValidateFlavorId(t *testing.T) { { description: "invalid flavor", flavorId: "foo", - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ {Id: utils.Ptr("bar-1")}, {Id: utils.Ptr("bar-2")}, {Id: utils.Ptr("bar-3")}, @@ -241,9 +239,9 @@ func TestValidateFlavorId(t *testing.T) { func TestLoadFlavorId(t *testing.T) { tests := []struct { description string - cpu int64 - ram int64 - flavors *[]sqlserverflex.InstanceFlavorEntry + cpu int32 + ram int32 + flavors []sqlserverflex.InstanceFlavorEntry isValid bool expectedOutput *string }{ @@ -251,21 +249,21 @@ func TestLoadFlavorId(t *testing.T) { description: "base", cpu: 2, ram: 4, - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(2)), }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("foo"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, isValid: true, @@ -282,14 +280,14 @@ func TestLoadFlavorId(t *testing.T) { description: "no flavors", cpu: 2, ram: 4, - flavors: &[]sqlserverflex.InstanceFlavorEntry{}, + flavors: []sqlserverflex.InstanceFlavorEntry{}, isValid: false, }, { description: "flavors with details missing", cpu: 2, ram: 4, - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr("bar-1"), Cpu: nil, @@ -297,13 +295,13 @@ func TestLoadFlavorId(t *testing.T) { }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, { Id: utils.Ptr("foo"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, isValid: true, @@ -313,21 +311,21 @@ func TestLoadFlavorId(t *testing.T) { description: "match with nil id", cpu: 2, ram: 4, - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(2)), }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, { Id: nil, - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(4)), }, }, isValid: false, @@ -336,16 +334,16 @@ func TestLoadFlavorId(t *testing.T) { description: "invalid settings", cpu: 2, ram: 4, - flavors: &[]sqlserverflex.InstanceFlavorEntry{ + flavors: []sqlserverflex.InstanceFlavorEntry{ { Id: utils.Ptr("bar-1"), - Cpu: utils.Ptr(int64(2)), - Memory: utils.Ptr(int64(2)), + Cpu: utils.Ptr(int32(2)), + Memory: utils.Ptr(int32(2)), }, { Id: utils.Ptr("bar-2"), - Cpu: utils.Ptr(int64(4)), - Memory: utils.Ptr(int64(4)), + Cpu: utils.Ptr(int32(4)), + Memory: utils.Ptr(int32(4)), }, }, isValid: false, @@ -404,12 +402,12 @@ func TestGetInstanceName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &sqlServerFlexClientMocked{ + settings := &mockSettings{ getInstanceFails: tt.getInstanceFails, getInstanceResp: tt.getInstanceResp, } - output, err := GetInstanceName(context.Background(), client, testProjectId, testInstanceId, testRegion) + output, err := GetInstanceName(context.Background(), newApiMock(settings), testProjectId, testInstanceId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") @@ -454,12 +452,12 @@ func TestGetUserName(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - client := &sqlServerFlexClientMocked{ + settings := &mockSettings{ getUserFails: tt.getUserFails, getUserResp: tt.getUserResp, } - output, err := GetUserName(context.Background(), client, testProjectId, testInstanceId, testUserId, testRegion) + output, err := GetUserName(context.Background(), newApiMock(settings), testProjectId, testInstanceId, testUserId, testRegion) if tt.isValid && err != nil { t.Errorf("failed on valid input") diff --git a/internal/pkg/services/vpn/client/client.go b/internal/pkg/services/vpn/client/client.go new file mode 100644 index 000000000..9eee90830 --- /dev/null +++ b/internal/pkg/services/vpn/client/client.go @@ -0,0 +1,14 @@ +package client + +import ( + "github.com/spf13/viper" + vpn "github.com/stackitcloud/stackit-sdk-go/services/vpn/v1api" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + genericclient "github.com/stackitcloud/stackit-cli/internal/pkg/generic-client" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +func ConfigureClient(p *print.Printer, cliVersion string) (*vpn.APIClient, error) { + return genericclient.ConfigureClientGeneric(p, cliVersion, viper.GetString(config.VPNCustomEndpointKey), false, vpn.NewAPIClient) +} diff --git a/internal/pkg/spinner/spinner.go b/internal/pkg/spinner/spinner.go index 9c530f1ca..fa76ef2b8 100644 --- a/internal/pkg/spinner/spinner.go +++ b/internal/pkg/spinner/spinner.go @@ -15,7 +15,30 @@ type Spinner struct { done chan bool } -func New(p *print.Printer) *Spinner { +// Run starts a spinner and stops it when f completes +func Run(p *print.Printer, message string, f func() error) error { + _, err := Run2(p, message, func() (struct{}, error) { + err := f() + return struct{}{}, err + }) + return err +} + +// Run2 starts a spinner and stops it when f (result arity 2) completes. +func Run2[T any](p *print.Printer, message string, f func() (T, error)) (T, error) { + var r T + spinner := newSpinner(p) + spinner.start(message) + r, err := f() + if err != nil { + spinner.stopWithError() + return r, err + } + spinner.stop() + return r, nil +} + +func newSpinner(p *print.Printer) *Spinner { return &Spinner{ printer: p, states: []string{"|", "/", "-", "\\"}, @@ -25,18 +48,18 @@ func New(p *print.Printer) *Spinner { } } -func (s *Spinner) Start(message string) { +func (s *Spinner) start(message string) { s.message = message go s.animate() } -func (s *Spinner) Stop() { +func (s *Spinner) stop() { s.done <- true close(s.done) s.printer.Info("\r%s ✓ \n", s.message) } -func (s *Spinner) StopWithError() { +func (s *Spinner) stopWithError() { s.done <- true close(s.done) s.printer.Info("\r%s ✗ \n", s.message) diff --git a/internal/pkg/testparams/testparams.go b/internal/pkg/testparams/testparams.go new file mode 100644 index 000000000..3a05ea326 --- /dev/null +++ b/internal/pkg/testparams/testparams.go @@ -0,0 +1,29 @@ +package testparams + +import ( + "bytes" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type TestParams struct { + *types.CmdParams + In, Out, Err *bytes.Buffer +} + +func NewTestParams() *TestParams { + in := &bytes.Buffer{} + out := &bytes.Buffer{} + err := &bytes.Buffer{} + return &TestParams{ + &types.CmdParams{ + Printer: print.NewPrinter( + in, out, err, + ), + }, + in, + out, + err, + } +} diff --git a/internal/pkg/testutils/assert.go b/internal/pkg/testutils/assert.go new file mode 100755 index 000000000..42c280a71 --- /dev/null +++ b/internal/pkg/testutils/assert.go @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package testutils + +// Package test provides utilities for validating CLI command test results with +// explicit helpers for error expectations and value comparisons. By splitting +// error and value handling the package keeps assertions simple and removes the +// need for dynamic type checks in every test case. +// +// Example usage: +// +// // Expect a specific error type +// if !test.AssertError(t, run(), &cliErr.FlagValidationError{}) { +// return +// } +// +// // Expect any error +// if !test.AssertError(t, run(), true) { +// return +// } +// +// // Expect error message substring +// if !test.AssertError(t, run(), "not found") { +// return +// } +// +// // Compare complex structs with private fields +// test.AssertValue(t, got, want, test.WithAllowUnexported(MyStruct{})) + +import ( + "errors" + "reflect" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +// AssertError verifies that an observed error satisfies the expected condition. +// +// Returns: +// - bool: True if the test should continue to value checks (i.e., no error occurred). +// +// Behavior: +// 1. If err is nil: +// - If want is nil or false: Success. +// - If want is anything else: Fails test (Expected error but got nil). +// 2. If err is non-nil: +// - If want is nil or false: Fails test (Unexpected error). +// - If want is true: Success (Any error accepted). +// - If want is string: Asserts err.Error() contains the string. +// - If want is error: Asserts errors.Is(err, want) or type match. +func AssertError(t testing.TB, got error, want any) bool { + t.Helper() + + // Case 1: No error occurred + if got == nil { + if want == nil || want == false { + return true + } + t.Errorf("got nil error, want %v", want) + return false + } + + // Case 2: Error occurred + if want == nil || want == false { + t.Errorf("got unexpected error: %v", got) + return false + } + + if want == true { + return false // Error expected and received, stop test + } + + // Handle string error type expectation + if wantStr, ok := want.(string); ok { + if !strings.Contains(got.Error(), wantStr) { + t.Errorf("got error %q, want substring %q", got, wantStr) + } + return false + } + + // Handle specific error type expectation + if wantErr, ok := want.(error); ok { + if checkErrorMatch(got, wantErr) { + return false + } + t.Errorf("got error %v, want %v", got, wantErr) + return false + } + + t.Errorf("invalid want type %T for AssertError", want) + return false +} + +func checkErrorMatch(got, want error) bool { + if errors.Is(got, want) { + return true + } + + // Fallback to type check using errors.As to handle wrapped errors + if want != nil { + typ := reflect.TypeOf(want) + // errors.As requires a pointer to the target type. + // reflect.New(typ) returns *T where T is the type of want. + target := reflect.New(typ).Interface() + if errors.As(got, target) { + return true + } + } + + return false +} + +// DiffFunc compares two values and returns a diff string. An empty string means +// equality. +type DiffFunc func(got, want any) string + +// ValueComparisonOption configures how HandleValueResult applies cmp options or +// diffing strategies. +type ValueComparisonOption func(*valueComparisonConfig) + +type valueComparisonConfig struct { + diffFunc DiffFunc + cmpOptions []cmp.Option +} + +func (config *valueComparisonConfig) getDiffFunc() DiffFunc { + if config.diffFunc != nil { + return config.diffFunc + } + return func(got, want any) string { + return cmp.Diff(got, want, config.cmpOptions...) + } +} + +// WithCmpOptions accumulates cmp.Options used during value comparison. +func WithAssertionCmpOptions(opts ...cmp.Option) ValueComparisonOption { + return func(config *valueComparisonConfig) { + config.cmpOptions = append(config.cmpOptions, opts...) + } +} + +// WithAllowUnexported enables comparison of unexported fields for the provided +// struct types. +func WithAllowUnexported(types ...any) ValueComparisonOption { + return WithAssertionCmpOptions(cmp.AllowUnexported(types...)) +} + +// WithDiffFunc sets a custom diffing function. Providing this option overrides +// the default cmp-based diff logic. +func WithDiffFunc(diffFunc DiffFunc) ValueComparisonOption { + return func(config *valueComparisonConfig) { + config.diffFunc = diffFunc + } +} + +// WithIgnoreFields ignores the specified fields on the provided type during comparison. +// It uses cmpopts.IgnoreFields to ensure type-safe filtering. +func WithIgnoreFields(typ any, names ...string) ValueComparisonOption { + return WithAssertionCmpOptions(cmpopts.IgnoreFields(typ, names...)) +} + +// AssertValue compares two values with cmp.Diff while allowing callers to +// tweak the diff strategy via ValueComparisonOption. A non-empty diff is +// reported as an error containing the diff output. +func AssertValue[T any](t testing.TB, got, want T, opts ...ValueComparisonOption) { + t.Helper() + // Configure comparison options + config := &valueComparisonConfig{} + for _, opt := range opts { + opt(config) + } + // Perform comparison and report diff + diff := config.getDiffFunc()(got, want) + if diff != "" { + t.Errorf("values do not match: %s", diff) + } +} diff --git a/internal/pkg/testutils/assert_test.go b/internal/pkg/testutils/assert_test.go new file mode 100755 index 000000000..ae683a54b --- /dev/null +++ b/internal/pkg/testutils/assert_test.go @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package testutils + +import ( + "errors" + "fmt" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp/cmpopts" +) + +type customError struct{ msg string } + +func (e *customError) Error() string { return e.msg } + +type anotherError struct{ code int } + +func (e *anotherError) Error() string { return fmt.Sprintf("code=%d", e.code) } + +type mockTB struct { + testing.TB + failed bool + msg string +} + +func (m *mockTB) Helper() {} +func (m *mockTB) Errorf(format string, args ...any) { + m.failed = true + m.msg = fmt.Sprintf(format, args...) +} + +func TestAssertError(t *testing.T) { + t.Parallel() + + sentinel := errors.New("sentinel") + + tests := map[string]struct { + got error // The input provided as got to AssertError() + want any // The input provided as want to AssertError() + wantErr bool // Whether this comparison is expected to fail + }{ + "exact match": { + got: &customError{msg: "boom"}, + want: &customError{}, + wantErr: false, + }, + "error string message match": { + got: errors.New("same message"), + want: "same message", + wantErr: false, + }, + "error string mismatch": { + got: errors.New("different"), + want: "same message", + wantErr: true, + }, + "sentinel via errors.Is": { + got: fmt.Errorf("wrap: %w", sentinel), + want: sentinel, + wantErr: false, + }, + "any error (true)": { + got: errors.New("any"), + want: true, + wantErr: false, + }, + "nil expectation (nil)": { + got: nil, + want: nil, + wantErr: false, + }, + "nil expectation (false)": { + got: nil, + want: false, + wantErr: false, + }, + "nil error input with error expectation": { + got: nil, + want: true, + wantErr: true, + }, + "unexpected error (nil want)": { + got: errors.New("unexpected"), + want: nil, + wantErr: true, + }, + "type match without message": { + got: &customError{msg: "alpha"}, + want: &customError{msg: "beta"}, + wantErr: false, + }, + "type mismatch": { + got: &customError{msg: "alpha"}, + want: &anotherError{}, + wantErr: true, + }, + "no error when none expected": { + got: nil, + want: false, + wantErr: false, + }, + "error but want false": { + got: errors.New("boom"), + want: false, + wantErr: true, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + mock := &mockTB{} + result := AssertError(mock, tt.got, tt.want) + + // if the test failed but we didn't expect it to fail + if mock.failed != tt.wantErr { + t.Fatalf("AssertError() failed = %v, wantErr %v (msg: %s)", mock.failed, tt.wantErr, mock.msg) + } + // if we expected an error the result of AssertError() should be false (this is what AssertError() does in case of error) + if tt.wantErr && result != false { + t.Fatalf("AssertError() returned = %v, want %v", result, tt.wantErr) + } + }) + } +} + +func TestCheckErrorMatch(t *testing.T) { + t.Parallel() + + underlying := &customError{msg: "root"} + wrapped := fmt.Errorf("wrap: %w", underlying) + if !checkErrorMatch(wrapped, &customError{}) { + t.Fatalf("expected wrapped customError to match via errors.As") + } + + notMatch := errors.New("other") + if checkErrorMatch(notMatch, &anotherError{}) { + t.Fatalf("expected mismatch for unrelated error types") + } +} + +func TestAssertValue(t *testing.T) { + t.Parallel() + + type payload struct { + Visible string + hidden int + } + + customDiff := func(got, want any) string { + if reflect.DeepEqual(got, want) { + return "" + } + return "custom diff" + } + + tests := []struct { + name string + got any // The input provided as got to AssertValue() + want any // The input provided as want to AssertValue() + wantErr bool // Whether this comparison is expected to fail + opts []ValueComparisonOption + }{ + { + name: "allow unexported success", + got: payload{Visible: "ok", hidden: 1}, + want: payload{Visible: "ok", hidden: 1}, + opts: []ValueComparisonOption{WithAllowUnexported(payload{})}, + }, + { + name: "allow unexported mismatch", + got: payload{Visible: "oops", hidden: 1}, + want: payload{Visible: "ok", hidden: 1}, + opts: []ValueComparisonOption{WithAllowUnexported(payload{})}, + wantErr: true, + }, + { + name: "cmp options sort", + got: []string{"b", "a", "c"}, + want: []string{"a", "b", "c"}, + opts: []ValueComparisonOption{WithAssertionCmpOptions(cmpopts.SortSlices(func(a, b string) bool { return a < b }))}, + }, + { + name: "custom diff mismatch", + got: 1, + want: 2, + opts: []ValueComparisonOption{WithDiffFunc(customDiff)}, + wantErr: true, + }, + { + name: "default diff success", + got: 42, + want: 42, + }, + { + name: "default diff mismatch", + got: 1, + want: 2, + wantErr: true, + }, + { + name: "diff func overrides cmp options", + got: []string{"b"}, + want: []string{"a"}, + opts: []ValueComparisonOption{ + WithAssertionCmpOptions(cmpopts.SortSlices(func(a, b string) bool { return a < b })), + WithDiffFunc(func(_, _ any) string { return "" }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + mock := &mockTB{} + AssertValue(mock, tt.got, tt.want, tt.opts...) + + // if the test failed but we didn't expect it to fail + if mock.failed != tt.wantErr { + t.Fatalf("AssertValue failed = %v, want %v (msg: %s)", mock.failed, tt.wantErr, mock.msg) + } + }) + } +} diff --git a/internal/pkg/testutils/options.go b/internal/pkg/testutils/options.go new file mode 100644 index 000000000..03d6dab2a --- /dev/null +++ b/internal/pkg/testutils/options.go @@ -0,0 +1,16 @@ +package testutils + +import "github.com/google/go-cmp/cmp" + +type Option struct { + cmpOptions []cmp.Option +} + +type TestingOption func(options *Option) error + +func WithCmpOptions(cmpOptions ...cmp.Option) TestingOption { + return func(options *Option) error { + options.cmpOptions = append(options.cmpOptions, cmpOptions...) + return nil + } +} diff --git a/internal/pkg/testutils/parse_input.go b/internal/pkg/testutils/parse_input.go new file mode 100755 index 000000000..394105da0 --- /dev/null +++ b/internal/pkg/testutils/parse_input.go @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package testutils + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +// ParseInputTestCase aggregates all required elements to exercise a CLI parseInput +// function. It centralizes the common flag setup, validation, and result +// assertions used throughout the edge command test suites. +type ParseInputTestCase[T any] struct { + Name string + // Args simulates positional arguments passed to the command. + Args []string + // Flags sets simple single-value flags. + Flags map[string]string + // RepeatFlags sets flags that can be specified multiple times (e.g. slice flags). + RepeatFlags map[string][]string + WantModel T + WantErr any + CmdFactory func(*types.CmdParams) *cobra.Command + // ParseInputFunc is the function under test. It must accept the printer, command, and args. + ParseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error) +} + +// ParseInputCaseOption allows configuring the test execution behavior. +type ParseInputCaseOption func(*parseInputCaseConfig) + +type parseInputCaseConfig struct { + cmpOpts []ValueComparisonOption +} + +// WithParseInputCmpOptions sets custom comparison options for AssertValue. +func WithParseInputCmpOptions(opts ...ValueComparisonOption) ParseInputCaseOption { + return func(cfg *parseInputCaseConfig) { + cfg.cmpOpts = append(cfg.cmpOpts, opts...) + } +} + +func defaultParseInputCaseConfig() *parseInputCaseConfig { + return &parseInputCaseConfig{} +} + +// RunParseInputCase executes a single parse-input test case using the provided +// configuration. It mirrors the typical table-driven pattern while removing the +// boilerplate repeated across tests. The helper short-circuits as soon as an +// expected error is encountered. +func RunParseInputCase[T any](t *testing.T, tc ParseInputTestCase[T], opts ...ParseInputCaseOption) { + t.Helper() + + cfg := defaultParseInputCaseConfig() + for _, opt := range opts { + opt(cfg) + } + + if tc.CmdFactory == nil { + t.Fatalf("parse input case %q missing CmdFactory", tc.Name) + } + if tc.ParseInputFunc == nil { + t.Fatalf("parse input case %q missing ParseInputFunc", tc.Name) + } + + params := testparams.NewTestParams() + cmd := tc.CmdFactory(params.CmdParams) + if cmd == nil { + t.Fatalf("parse input case %q produced nil command", tc.Name) + } + + if err := globalflags.Configure(cmd.Flags()); err != nil { + t.Fatalf("configure global flags: %v", err) + } + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if flag.Value == nil { + return + } + if r, ok := flag.Value.(Resettable); ok { + r.Reset() + } + }) + + // Set regular flag values. + for flag, value := range tc.Flags { + if err := cmd.Flags().Set(flag, value); err != nil { + AssertError(t, err, tc.WantErr) + return + } + } + + // Set repeated flag values. + for flag, values := range tc.RepeatFlags { + for _, value := range values { + if err := cmd.Flags().Set(flag, value); err != nil { + AssertError(t, err, tc.WantErr) + return + } + } + } + + // Test cobra argument validation. + if err := cmd.ValidateArgs(tc.Args); err != nil { + AssertError(t, err, tc.WantErr) + return + } + + // Test cobra required flags validation. + if err := cmd.ValidateRequiredFlags(); err != nil { + AssertError(t, err, tc.WantErr) + return + } + + // Test cobra flag group validation. + if err := cmd.ValidateFlagGroups(); err != nil { + AssertError(t, err, tc.WantErr) + return + } + + // Test parse input function. + got, err := tc.ParseInputFunc(params.Printer, cmd, tc.Args) + if !AssertError(t, err, tc.WantErr) { + return + } + + AssertValue(t, got, tc.WantModel, cfg.cmpOpts...) +} diff --git a/internal/pkg/testutils/parse_input_test.go b/internal/pkg/testutils/parse_input_test.go new file mode 100755 index 000000000..6b5d6c36b --- /dev/null +++ b/internal/pkg/testutils/parse_input_test.go @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2025 STACKIT GmbH & Co. KG + +package testutils + +import ( + "errors" + "testing" + + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" +) + +type parseInputTestModel struct { + Value string + Args []string + RepeatValue []string + hidden string +} + +func newTestCmdFactory(flagSetup func(*cobra.Command)) func(*types.CmdParams) *cobra.Command { + return func(*types.CmdParams) *cobra.Command { + cmd := &cobra.Command{Use: "test"} + if flagSetup != nil { + flagSetup(cmd) + } + return cmd + } +} + +func TestRunParseInputCase(t *testing.T) { + sentinel := errors.New("parse failed") + tests := []struct { + name string + flagSetup func(*cobra.Command) + flags map[string]string + repeatFlags map[string][]string + args []string + cmpOpts []ParseInputCaseOption + wantModel *parseInputTestModel + wantErr any + parseFunc func(*print.Printer, *cobra.Command, []string) (*parseInputTestModel, error) + expectParseCall bool + }{ + { + name: "success", + flagSetup: func(cmd *cobra.Command) { + cmd.Flags().String("name", "", "") + }, + flags: map[string]string{"name": "edge"}, + cmpOpts: []ParseInputCaseOption{WithParseInputCmpOptions(WithAllowUnexported(parseInputTestModel{}))}, + wantModel: &parseInputTestModel{Value: "edge", hidden: "protected"}, + parseFunc: func(_ *print.Printer, cmd *cobra.Command, _ []string) (*parseInputTestModel, error) { + val, _ := cmd.Flags().GetString("name") + return &parseInputTestModel{Value: val, hidden: "protected"}, nil + }, + expectParseCall: true, + }, + { + name: "flag set failure", + flagSetup: func(cmd *cobra.Command) { + cmd.Flags().Int("count", 0, "") + }, + flags: map[string]string{"count": "invalid"}, + wantErr: "invalid syntax", + parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { + return &parseInputTestModel{}, nil + }, + expectParseCall: false, + }, + { + name: "flag group validation", + flagSetup: func(cmd *cobra.Command) { + cmd.Flags().String("first", "", "") + cmd.Flags().String("second", "", "") + cmd.MarkFlagsRequiredTogether("first", "second") + }, + flags: map[string]string{"first": "only"}, + wantErr: "must all be set", + parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { + return &parseInputTestModel{}, nil + }, + expectParseCall: false, + }, + { + name: "parse func error", + flagSetup: func(cmd *cobra.Command) { + cmd.Flags().Bool("ok", false, "") + }, + flags: map[string]string{"ok": "true"}, + wantErr: sentinel, + parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { + return nil, sentinel + }, + expectParseCall: true, + }, + { + name: "args success", + flagSetup: func(cmd *cobra.Command) { + cmd.Args = cobra.ExactArgs(1) + }, + args: []string{"arg1"}, + cmpOpts: []ParseInputCaseOption{WithParseInputCmpOptions(WithAllowUnexported(parseInputTestModel{}))}, + wantModel: &parseInputTestModel{Args: []string{"arg1"}}, + parseFunc: func(_ *print.Printer, _ *cobra.Command, args []string) (*parseInputTestModel, error) { + return &parseInputTestModel{Args: args}, nil + }, + expectParseCall: true, + }, + { + name: "args validation failure", + flagSetup: func(cmd *cobra.Command) { + cmd.Args = cobra.NoArgs + }, + args: []string{"arg1"}, + wantErr: "unknown command", + parseFunc: func(_ *print.Printer, _ *cobra.Command, _ []string) (*parseInputTestModel, error) { + return &parseInputTestModel{}, nil + }, + expectParseCall: false, + }, + { + name: "repeat flags success", + flagSetup: func(cmd *cobra.Command) { + cmd.Flags().StringSlice("tags", []string{}, "") + }, + repeatFlags: map[string][]string{"tags": {"tag1", "tag2"}}, + cmpOpts: []ParseInputCaseOption{WithParseInputCmpOptions(WithAllowUnexported(parseInputTestModel{}))}, + wantModel: &parseInputTestModel{RepeatValue: []string{"tag1", "tag2"}}, + parseFunc: func(_ *print.Printer, cmd *cobra.Command, _ []string) (*parseInputTestModel, error) { + val, _ := cmd.Flags().GetStringSlice("tags") + return &parseInputTestModel{RepeatValue: val}, nil + }, + expectParseCall: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdFactory := newTestCmdFactory(tt.flagSetup) + var parseCalled bool + parseFn := tt.parseFunc + if parseFn == nil { + parseFn = func(*print.Printer, *cobra.Command, []string) (*parseInputTestModel, error) { + return &parseInputTestModel{}, nil + } + } + + RunParseInputCase(t, ParseInputTestCase[*parseInputTestModel]{ + Name: tt.name, + Flags: tt.flags, + RepeatFlags: tt.repeatFlags, + Args: tt.args, + WantModel: tt.wantModel, + WantErr: tt.wantErr, + CmdFactory: cmdFactory, + ParseInputFunc: func(pr *print.Printer, cmd *cobra.Command, args []string) (*parseInputTestModel, error) { + parseCalled = true + return parseFn(pr, cmd, args) + }, + }, tt.cmpOpts...) + + if parseCalled != tt.expectParseCall { + t.Fatalf("parseCalled = %v, expect %v", parseCalled, tt.expectParseCall) + } + }) + } +} diff --git a/internal/pkg/testutils/testutils.go b/internal/pkg/testutils/testutils.go new file mode 100644 index 000000000..74a07befc --- /dev/null +++ b/internal/pkg/testutils/testutils.go @@ -0,0 +1,138 @@ +package testutils + +import ( + "testing" + + "github.com/spf13/pflag" + + "github.com/stackitcloud/stackit-cli/internal/pkg/testparams" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/google/go-cmp/cmp" + "github.com/spf13/cobra" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" +) + +// TestParseInput centralizes the logic to test a combination of inputs (arguments, flags) for a cobra command +func TestParseInput[T any](t *testing.T, cmdFactory func(*types.CmdParams) *cobra.Command, parseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error), expectedModel T, argValues []string, flagValues map[string]string, isValid bool) { + t.Helper() + TestParseInputWithAdditionalFlags(t, cmdFactory, parseInputFunc, expectedModel, argValues, flagValues, map[string][]string{}, isValid) +} + +// TestParseInputWithAdditionalFlags centralizes the logic to test a combination of inputs (arguments, flags) for a cobra command. +// It allows to pass multiple instances of a single flag to the cobra command using the `additionalFlagValues` parameter. +func TestParseInputWithAdditionalFlags[T any](t *testing.T, cmdFactory func(*types.CmdParams) *cobra.Command, parseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error), expectedModel T, argValues []string, flagValues map[string]string, additionalFlagValues map[string][]string, isValid bool) { + TestParseInputWithOptions(t, cmdFactory, parseInputFunc, expectedModel, argValues, flagValues, additionalFlagValues, isValid, nil) +} + +type Resettable interface { + Reset() +} + +func TestParseInputWithOptions[T any](t *testing.T, cmdFactory func(*types.CmdParams) *cobra.Command, parseInputFunc func(*print.Printer, *cobra.Command, []string) (T, error), expectedModel T, argValues []string, flagValues map[string]string, additionalFlagValues map[string][]string, isValid bool, testingOptions []TestingOption) { + opts := Option{} + for _, option := range testingOptions { + err := option(&opts) + if err != nil { + t.Errorf("Configuring testing options: %v", err) + return + } + } + + params := testparams.NewTestParams() + cmd := cmdFactory(params.CmdParams) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + cmd.Flags().VisitAll(func(flag *pflag.Flag) { + if flag.Value == nil { + return + } + // StringEnum and StringEnumSlice Flags are stateful singletons. During tests we reset their state. + if r, ok := flag.Value.(Resettable); ok { + r.Reset() + } + }) + + // set regular flag values + for flag, value := range flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + // set additional flag values + for flag, values := range additionalFlagValues { + for _, value := range values { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + } + + if cmd.PreRun != nil { + // can be used for dynamic flag configuration + cmd.PreRun(cmd, argValues) + } + + if cmd.PreRunE != nil { + err := cmd.PreRunE(cmd, argValues) + if err != nil { + if !isValid { + return + } + t.Fatalf("error in PreRunE: %v", err) + } + } + + err = cmd.ValidateArgs(argValues) + if err != nil { + if !isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + err = cmd.ValidateFlagGroups() + if err != nil { + if !isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInputFunc(params.Printer, cmd, argValues) + if err != nil { + if !isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, expectedModel, opts.cmpOptions...) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } +} diff --git a/internal/cmd/params/cmd_params.go b/internal/pkg/types/cmd_params.go similarity index 63% rename from internal/cmd/params/cmd_params.go rename to internal/pkg/types/cmd_params.go index 572c80706..f84c03f3e 100644 --- a/internal/cmd/params/cmd_params.go +++ b/internal/pkg/types/cmd_params.go @@ -1,10 +1,15 @@ -package params +package types import ( + "io/fs" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) type CmdParams struct { Printer *print.Printer CliVersion string + Date string + Fs fs.FS + Args []string } diff --git a/internal/pkg/utils/os_fs.go b/internal/pkg/utils/os_fs.go new file mode 100644 index 000000000..790976108 --- /dev/null +++ b/internal/pkg/utils/os_fs.go @@ -0,0 +1,18 @@ +package utils + +import ( + "io/fs" + "os" +) + +var _ fs.ReadFileFS = OsFS{} + +type OsFS struct{} + +func (o OsFS) Open(name string) (fs.File, error) { + return os.Open(name) +} + +func (o OsFS) ReadFile(name string) ([]byte, error) { + return os.ReadFile(name) +} diff --git a/internal/pkg/utils/strings.go b/internal/pkg/utils/strings.go index 401287fa1..8772e38af 100644 --- a/internal/pkg/utils/strings.go +++ b/internal/pkg/utils/strings.go @@ -1,6 +1,8 @@ package utils import ( + "fmt" + "slices" "strings" "unicode/utf8" ) @@ -26,13 +28,43 @@ func JoinStringKeysPtr(m map[string]any, sep string) string { return JoinStringKeys(m, sep) } +// JoinStringMap concatenates the key-value pairs of a string map, key and value separated by keyValueSeparator, key value pairs separated by separator. +func JoinStringMap(m map[string]string, keyValueSeparator, separator string) string { + if m == nil { + return "" + } + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + slices.Sort(keys) + parts := make([]string, 0, len(m)) + for _, k := range keys { + parts = append(parts, fmt.Sprintf("%s%s%s", k, keyValueSeparator, m[k])) + } + return strings.Join(parts, separator) +} + // JoinStringPtr concatenates the strings of a string slice pointer, each separatore by the // [sep] string. -func JoinStringPtr(vals *[]string, sep string) string { +func JoinStringPtr[T ~string](vals *[]T, sep string) string { if vals == nil || len(*vals) == 0 { return "" } - return strings.Join(*vals, sep) + deref := *vals + switch len(deref) { + case 0: + return "" + case 1: + return string(deref[0]) + } + var b strings.Builder + b.WriteString(string(deref[0])) + for _, s := range deref[1:] { + b.WriteString(sep) + b.WriteString(string(s)) + } + return b.String() } // Truncate trims the passed string (if it is not nil). If the input string is diff --git a/internal/pkg/utils/strings_test.go b/internal/pkg/utils/strings_test.go index a7fb023bc..6f0279045 100644 --- a/internal/pkg/utils/strings_test.go +++ b/internal/pkg/utils/strings_test.go @@ -30,3 +30,39 @@ func TestTruncate(t *testing.T) { }) } } + +func TestJoinStringMap(t *testing.T) { + tests := []struct { + name string + input map[string]string + want string + }{ + { + name: "nil map", + input: nil, + want: "", + }, + { + name: "empty map", + input: map[string]string{}, + want: "", + }, + { + name: "single element", + input: map[string]string{"key1": "value1"}, + want: "key1=value1", + }, + { + name: "multiple elements", + input: map[string]string{"key1": "value1", "key2": "value2"}, + want: "key1=value1, key2=value2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := JoinStringMap(tt.input, "=", ", "); got != tt.want { + t.Errorf("JoinStringMap() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index 2db0936b8..f216efe1c 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "fmt" "net/url" + "slices" "strings" "time" @@ -11,8 +12,10 @@ import ( "github.com/inhies/go-bytesize" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" + sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) // Ptr Returns the pointer to any type T @@ -75,16 +78,35 @@ func ConvertInt64PToFloat64P(i *int64) *float64 { return &f } +// ConvertInt32PToFloat32P converts an int32 pointer to a float64 pointer +// This function will return nil if the input is nil +// This is a lossy conversion for i > 2^24 +func ConvertInt32PToFloat32P(i *int32) *float32 { + if i == nil { + return nil + } + f := float32(*i) + return &f +} + func ValidateURLDomain(value string) error { urlStruct, err := url.Parse(value) if err != nil { return fmt.Errorf("parse url: %w", err) } + urlHost := urlStruct.Hostname() if urlHost == "" { return fmt.Errorf("bad url") } + allowedSchemes := []string{ + "https", + } + if !slices.Contains(allowedSchemes, urlStruct.Scheme) { + return fmt.Errorf("unsupported protocol: %s", urlStruct.Scheme) + } + allowedUrlDomain := viper.GetString(config.AllowedUrlDomainKey) if !strings.HasSuffix(urlHost, allowedUrlDomain) { @@ -153,3 +175,34 @@ func ConvertStringMapToInterfaceMap(m *map[string]string) *map[string]interface{ } return &result } + +// GetSliceFromPointer returns the value of a pointer to a slice of type T. +// If the pointer is nil, it returns an empty slice. +func GetSliceFromPointer[T any](s *[]T) []T { + if s == nil || *s == nil { + return []T{} + } + return *s +} + +// FormatPossibleValues formats a slice into a list for usage in the provider docs +func FormatPossibleValues(values ...string) []string { + var formattedValues []string + for _, value := range values { + if value == "unknown_default_open_api" { + continue + } + + formattedValues = append(formattedValues, value) + } + return formattedValues +} + +// Map applies mapFn to each element in input and collects the results in a new slice +func Map[T, U any](input []T, mapFn func(T) U) []U { + values := make([]U, len(input)) + for i := range input { + values[i] = mapFn(input[i]) + } + return values +} diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 86588bad8..b627c9321 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -1,12 +1,14 @@ package utils import ( + "fmt" "reflect" "testing" + "github.com/spf13/viper" sdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/spf13/viper" "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) @@ -52,6 +54,57 @@ func TestConvertInt64PToFloat64P(t *testing.T) { } } +func TestConvertInt32PToFloat64P(t *testing.T) { + tests := []struct { + name string + input *int32 + expected *float32 + }{ + { + name: "positive", + input: utils.Ptr(int32(1)), + expected: utils.Ptr(float32(1)), + }, + { + name: "negative", + input: utils.Ptr(int32(-1)), + expected: utils.Ptr(float32(-1)), + }, + { + name: "zero", + input: utils.Ptr(int32(0)), + expected: utils.Ptr(float32(0)), + }, + { + name: "nil", + input: nil, + expected: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expected := ConvertInt32PToFloat32P(tt.input) + + if expected == nil && tt.expected == nil && tt.input == nil { + return + } + + if *expected != *tt.expected { + t.Errorf("ConvertInt64ToFloat64() = %v, want %v", *expected, *tt.expected) + } + }) + } +} + +func TestConvertInt32PToFloat64PLossyConversion(t *testing.T) { + i := int32(900_000_001) + f := ConvertInt32PToFloat32P(&i) + s := fmt.Sprintf("%f", *f) + if s != "900000000.000000" { + t.Errorf("Expected lossy conversion of %d to %f, got %s", i, *f, s) + } +} + func TestValidateURLDomain(t *testing.T) { tests := []struct { name string @@ -94,6 +147,21 @@ func TestValidateURLDomain(t *testing.T) { input: "", isValid: false, }, + { + name: "invalid protocol", + input: "http://example.stackit.cloud", + isValid: false, + }, + { + name: "no protocol", + input: "example.stackit.cloud", + isValid: false, + }, + { + name: "valid endpoint", + input: "https://service-account.api.stackit.cloud/token", + isValid: true, + }, } for _, tt := range tests { @@ -248,3 +316,87 @@ func TestConvertStringMapToInterfaceMap(t *testing.T) { }) } } + +func TestGetSliceFromPointer(t *testing.T) { + tests := []struct { + name string + input *[]string + expected []string + }{ + { + name: "nil pointer", + input: nil, + expected: []string{}, + }, + { + name: "pointer to nil slice", + input: func() *[]string { + var s []string + return &s + }(), + expected: []string{}, + }, + { + name: "empty slice", + input: &[]string{}, + expected: []string{}, + }, + { + name: "populated slice", + input: &[]string{"item1", "item2"}, + expected: []string{"item1", "item2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := GetSliceFromPointer(tt.input) + + if result == nil { + t.Errorf("GetSliceFromPointer() = %v, want %v", result, tt.expected) + return + } + + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("GetSliceFromPointer() = %v, want %v", result, tt.expected) + } + }) + } +} + +func TestMap(t *testing.T) { + type args[T any, U any] struct { + input []T + mapFn func(T) U + } + type testCase[T any, U any] struct { + name string + args args[T, U] + want []U + } + tests := []testCase[string, *string]{ + { + name: "default", + args: args[string, *string]{ + input: []string{"foo", "bar"}, + mapFn: Ptr[string], + }, + want: []*string{Ptr("foo"), Ptr("bar")}, + }, + { + name: "input slice is nil", + args: args[string, *string]{ + input: nil, + mapFn: Ptr[string], + }, + want: []*string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Map(tt.args.input, tt.args.mapFn); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Map() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/main.go b/main.go index 5286783ac..177afc4ba 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,13 @@ package main import ( + "os" + "github.com/stackitcloud/stackit-cli/internal/cmd" "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" ) // These values are overwritten by GoReleaser at build time @@ -15,5 +20,19 @@ func main() { // Set up configuration files config.InitConfig() - cmd.Execute(version, date) + printer := print.NewPrinter( + os.Stdin, + os.Stdout, + os.Stderr, + ) + params := types.CmdParams{ + Printer: printer, + CliVersion: version, + Date: date, + Fs: utils.OsFS{}, + Args: os.Args[1:], + } + if !cmd.Execute(¶ms) { + os.Exit(1) + } } diff --git a/scripts/check-docs.sh b/scripts/check-docs.sh index d81181db9..cb2058804 100755 --- a/scripts/check-docs.sh +++ b/scripts/check-docs.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This script is used to ensure for PRs the docs are up-to-date via the CI pipeline # Usage: ./check-docs.sh diff --git a/scripts/generate.go b/scripts/generate.go index 99d930910..128938924 100644 --- a/scripts/generate.go +++ b/scripts/generate.go @@ -9,6 +9,8 @@ import ( "strings" "github.com/stackitcloud/stackit-cli/internal/cmd" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra/doc" ) @@ -38,7 +40,16 @@ func main() { linkHandler := func(filename string) string { return fmt.Sprintf("./%s", filename) } - err = doc.GenMarkdownTreeCustom(cmd.NewRootCmd("", "", nil), docsDir, filePrepender, linkHandler) + printer := print.NewPrinter( + os.Stdin, + os.Stdout, + os.Stderr, + ) + params := &types.CmdParams{ + Printer: printer, + Args: os.Args, + } + err = doc.GenMarkdownTreeCustom(cmd.NewRootCmd(params), docsDir, filePrepender, linkHandler) if err != nil { log.Fatalf("Error generating documentation: %v", err) } diff --git a/scripts/publish-apt-packages.sh b/scripts/publish-apt-packages.sh index f6ec84174..81aa53cb4 100755 --- a/scripts/publish-apt-packages.sh +++ b/scripts/publish-apt-packages.sh @@ -1,11 +1,9 @@ -#!/bin/bash +#!/usr/bin/env bash # This script is used to publish new packages to the CLI APT repository # Usage: ./publish-apt-packages.sh set -eo pipefail -ROOT_DIR=$(git rev-parse --show-toplevel) - PACKAGES_BUCKET_URL="https://packages.stackit.cloud" PUBLIC_KEY_FILE_PATH="keys/key.gpg" APT_REPO_PATH="apt/cli" @@ -27,7 +25,7 @@ aptly mirror create -config "${APTLY_CONFIG_FILE_PATH}" -keyring="${CUSTOM_KEYRI # Update the mirror to the latest state printf "\n>>> Updating mirror \n" -aptly mirror update -keyring="${CUSTOM_KEYRING_FILE}" current +aptly mirror update -keyring="${CUSTOM_KEYRING_FILE}" -max-tries=5 current # Create a snapshot of the mirror printf "\n>>> Creating snapshop from mirror \n" @@ -51,4 +49,4 @@ aptly snapshot pull -no-remove -architectures="amd64,i386,arm64" current-snapsho # Publish the new snapshot to the remote repo printf "\n>>> Publishing updated snapshot \n" -aptly publish snapshot -keyring="${CUSTOM_KEYRING_FILE}" -gpg-key="${GPG_PRIVATE_KEY_FINGERPRINT}" -passphrase "${GPG_PASSPHRASE}" -config "${APTLY_CONFIG_FILE_PATH}" updated-snapshot "s3:${APT_BUCKET_NAME}:${APT_REPO_PATH}" +aptly publish snapshot -keyring="${CUSTOM_KEYRING_FILE}" -gpg-key="${GPG_PRIVATE_KEY_FINGERPRINT}" -passphrase "${GPG_PASSPHRASE}" -config "${APTLY_CONFIG_FILE_PATH}" updated-snapshot "s3:${APT_BUCKET_NAME}:${APT_REPO_PATH}" \ No newline at end of file diff --git a/scripts/publish-rpm-packages.sh b/scripts/publish-rpm-packages.sh new file mode 100755 index 000000000..d657d1e0d --- /dev/null +++ b/scripts/publish-rpm-packages.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash + +# This script is used to publish new RPM packages to the CLI RPM repository +# Usage: ./publish-rpm-packages.sh +set -eo pipefail + +PACKAGES_BUCKET_URL="https://packages.stackit.cloud" +PUBLIC_KEY_FILE_PATH="keys/key.gpg" +RPM_REPO_PATH="rpm/cli" +RPM_BUCKET_NAME="distribution" +GORELEASER_PACKAGES_FOLDER="dist/" + +# We need to disable the key database daemon (keyboxd) +# This can be done by removing "use-keyboxd" from ~/.gnupg/common.conf (see https://github.com/gpg/gnupg/blob/master/README) +echo -n >~/.gnupg/common.conf + +# Create RPM repository directory structure +printf ">>> Creating RPM repository structure \n" +mkdir -p rpm-repo/x86_64 +mkdir -p rpm-repo/i386 +mkdir -p rpm-repo/aarch64 + +# Copy RPM packages to appropriate architecture directories +printf "\n>>> Copying RPM packages to architecture directories \n" + +# Copy x86_64 packages (amd64) +for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_amd64.rpm; do + if [ -f "$rpm_file" ]; then + cp "$rpm_file" rpm-repo/x86_64/ + printf "Copied %s to x86_64/\n" "$(basename "$rpm_file")" + fi +done + +# Copy i386 packages +for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_386.rpm; do + if [ -f "$rpm_file" ]; then + cp "$rpm_file" rpm-repo/i386/ + printf "Copied %s to i386/\n" "$(basename "$rpm_file")" + fi +done + +# Copy aarch64 packages (arm64) +for rpm_file in "${GORELEASER_PACKAGES_FOLDER}"*_arm64.rpm; do + if [ -f "$rpm_file" ]; then + cp "$rpm_file" rpm-repo/aarch64/ + printf "Copied %s to aarch64/\n" "$(basename "$rpm_file")" + fi +done + +# Download existing repository content (RPMs and metadata) if it exists +printf "\n>>> Downloading existing repository content \n" +aws s3 sync s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ rpm-repo/ --endpoint-url "${AWS_ENDPOINT_URL}" --exclude "*.asc" || echo "No existing repository found, creating new one" + +# Create repository metadata for each architecture +printf "\n>>> Creating repository metadata \n" +for arch in x86_64 i386 aarch64; do + if [ -d "rpm-repo/${arch}" ] && [ -n "$(find "rpm-repo/${arch}" -mindepth 1 -maxdepth 1 -print -quit)" ]; then + printf "Creating metadata for %s...\n" "$arch" + + # List what we're working with + file_list=$(find "rpm-repo/${arch}" -maxdepth 1 -type f -exec basename {} \; | tr '\n' ' ') + printf "Files in %s: %s\n" "$arch" "${file_list% }" + + # Create repository metadata + createrepo_c --update rpm-repo/${arch} + + # Sign the repository metadata + printf "Signing repository metadata for %s...\n" "$arch" + # Remove existing signature file if it exists + rm -f rpm-repo/${arch}/repodata/repomd.xml.asc + gpg --batch --pinentry-mode loopback --detach-sign --armor \ + --local-user "${GPG_PRIVATE_KEY_FINGERPRINT}" \ + --passphrase "${GPG_PASSPHRASE}" \ + rpm-repo/${arch}/repodata/repomd.xml + + # Verify the signature was created + if [ -f "rpm-repo/${arch}/repodata/repomd.xml.asc" ]; then + printf "Repository metadata signed successfully for %s\n" "$arch" + else + printf "WARNING: Repository metadata signature not created for %s\n" "$arch" + fi + else + printf "No packages found for %s, skipping...\n" "$arch" + fi +done + +# Upload the updated repository to S3 in two phases (repodata pointers last) +# clients reading the repo won't see a state where repomd.xml points to files not uploaded yet. +printf "\n>>> Uploading repository to S3 (phase 1: all except repomd*) \n" +aws s3 sync rpm-repo/ s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ \ + --endpoint-url "${AWS_ENDPOINT_URL}" \ + --delete \ + --exclude "*/repodata/repomd.xml" \ + --exclude "*/repodata/repomd.xml.asc" + +printf "\n>>> Uploading repository to S3 (phase 2: repomd* only) \n" +aws s3 sync rpm-repo/ s3://${RPM_BUCKET_NAME}/${RPM_REPO_PATH}/ \ + --endpoint-url "${AWS_ENDPOINT_URL}" \ + --exclude "*" \ + --include "*/repodata/repomd.xml" \ + --include "*/repodata/repomd.xml.asc" + +# Upload the public key +# Also uploaded in APT publish; intentionally redundant +# Safe to overwrite and ensures updates if APT fails or key changes. +printf "\n>>> Uploading public key \n" +gpg --armor --export "${GPG_PRIVATE_KEY_FINGERPRINT}" > public-key.asc +aws s3 cp public-key.asc s3://${RPM_BUCKET_NAME}/${PUBLIC_KEY_FILE_PATH} --endpoint-url "${AWS_ENDPOINT_URL}" + +printf "\n>>> RPM repository published successfully! \n" +printf "Repository URL: %s/%s/ \n" "$PACKAGES_BUCKET_URL" "$RPM_REPO_PATH" +printf "Public key URL: %s/%s \n" "$PACKAGES_BUCKET_URL" "$PUBLIC_KEY_FILE_PATH" diff --git a/scripts/replace.sh b/scripts/replace.sh index 0c37f4b85..9326b1f72 100755 --- a/scripts/replace.sh +++ b/scripts/replace.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Add replace directives to local files to go.work set -eo pipefail